├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── custom.md │ ├── docs.md │ ├── i-d-like-to-request-a-feature.md │ ├── bug_report.yaml │ └── feature_request.yaml ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── labeler.yml │ ├── update-gradle-wrapper.yml │ ├── publish-gradle-plugin.yml │ ├── publish-converters.yml │ ├── gh-pages.yml │ └── preview-docs.yaml ├── dependabot.yml └── labeler.yml ├── ktorfit-lib ├── api │ ├── jvm │ │ └── ktorfit-lib.api │ ├── android │ │ └── ktorfit-lib.api │ └── ktorfit-lib.klib.api ├── .gitignore ├── gradle.properties └── src │ ├── androidMain │ └── AndroidManifest.xml │ └── commonMain │ └── kotlin │ └── de │ └── jensklingenberg │ └── ktorfit │ └── Empty.kt ├── example ├── AndroidOnlyExample │ ├── app │ │ ├── .gitignore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ ├── themes.xml │ │ │ │ │ │ └── colors.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ └── ic_launcher_round.webp │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ └── ic_launcher_round.webp │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ └── ic_launcher_round.webp │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ └── ic_launcher_round.webp │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.webp │ │ │ │ │ │ └── ic_launcher_round.webp │ │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ │ ├── ic_launcher.xml │ │ │ │ │ │ └── ic_launcher_round.xml │ │ │ │ │ ├── xml │ │ │ │ │ │ ├── backup_rules.xml │ │ │ │ │ │ └── data_extraction_rules.xml │ │ │ │ │ └── drawable-v24 │ │ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ │ ├── java │ │ │ │ │ └── de │ │ │ │ │ │ └── jensklingenberg │ │ │ │ │ │ └── androidonlyexample │ │ │ │ │ │ ├── ui │ │ │ │ │ │ └── theme │ │ │ │ │ │ │ ├── Color.kt │ │ │ │ │ │ │ ├── Shape.kt │ │ │ │ │ │ │ ├── Type.kt │ │ │ │ │ │ │ └── Theme.kt │ │ │ │ │ │ ├── GitHubService.kt │ │ │ │ │ │ ├── Person.kt │ │ │ │ │ │ ├── TestJava.java │ │ │ │ │ │ └── StarWarsApi.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── test │ │ │ │ └── java │ │ │ │ │ └── de │ │ │ │ │ └── jensklingenberg │ │ │ │ │ └── androidonlyexample │ │ │ │ │ └── ExampleUnitTest.kt │ │ │ └── androidTest │ │ │ │ └── java │ │ │ │ └── de │ │ │ │ └── jensklingenberg │ │ │ │ └── androidonlyexample │ │ │ │ └── ExampleInstrumentedTest.kt │ │ └── proguard-rules.pro │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── build.gradle.kts │ ├── .gitignore │ ├── settings.gradle.kts │ └── gradle.properties └── MultiplatformExample │ ├── Readme.md │ ├── iosApp │ ├── iosApp │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── iOSApp.swift │ │ ├── ContentView.swift │ │ └── Info.plist │ ├── Podfile │ ├── Pods │ │ ├── Target Support Files │ │ │ ├── Pods-iosApp │ │ │ │ ├── Pods-iosApp.modulemap │ │ │ │ ├── Pods-iosApp-dummy.m │ │ │ │ ├── Pods-iosApp-acknowledgements.markdown │ │ │ │ ├── Pods-iosApp-umbrella.h │ │ │ │ ├── Pods-iosApp.debug.xcconfig │ │ │ │ ├── Pods-iosApp.release.xcconfig │ │ │ │ ├── Pods-iosApp-acknowledgements.plist │ │ │ │ └── Pods-iosApp-Info.plist │ │ │ └── shared │ │ │ │ ├── shared.debug.xcconfig │ │ │ │ └── shared.release.xcconfig │ │ ├── Manifest.lock │ │ ├── Pods.xcodeproj │ │ │ └── xcuserdata │ │ │ │ └── jensklingenberg.xcuserdatad │ │ │ │ └── xcschemes │ │ │ │ └── xcschememanagement.plist │ │ └── Local Podspecs │ │ │ └── shared.podspec.json │ ├── iosApp.xcworkspace │ │ ├── xcuserdata │ │ │ └── jens.klingenberg.xcuserdatad │ │ │ │ └── xcschemes │ │ │ │ └── xcschememanagement.plist │ │ └── contents.xcworkspacedata │ ├── Podfile.lock │ └── iosApp.xcodeproj │ │ └── xcuserdata │ │ └── jens.klingenberg.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── iosApp.xcscheme │ ├── person │ └── src │ │ ├── androidMain │ │ └── AndroidManifest.xml │ │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── ktorfittest │ │ └── Person.kt │ ├── shared │ └── src │ │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── ktorfittest │ │ │ ├── Platform.kt │ │ │ └── Test.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── ktorfittest │ │ │ ├── Platform.kt │ │ │ ├── StarWarsApi.kt │ │ │ └── Greeting.kt │ │ ├── macosX64Main │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── ktorfittest │ │ │ └── Platform.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── ktorfittest │ │ │ ├── Platform.kt │ │ │ └── main.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ ├── com │ │ │ └── example │ │ │ │ └── ktorfittest │ │ │ │ ├── Platform.kt │ │ │ │ └── StarWarsApi.kt │ │ │ └── JvmExampleClass.kt │ │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ └── ktorfittest │ │ │ └── Platform.kt │ │ └── commonTest │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── ktorfittest │ │ ├── TestApi.kt │ │ └── KtorfitTest.kt │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── androidApp │ ├── src │ │ └── main │ │ │ ├── res │ │ │ ├── values │ │ │ │ ├── colors.xml │ │ │ │ └── styles.xml │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── myapplication │ │ │ │ └── android │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ └── build.gradle.kts │ ├── gradle.properties │ ├── settings.gradle.kts │ └── build.gradle.kts ├── ktorfit-compiler-plugin ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ └── java │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ ├── DebugLogger.kt │ │ ├── KtorfitIrGenerationExtension.kt │ │ ├── CommonCompilerPluginRegistrar.kt │ │ ├── ExampleCommandLineProcessor.kt │ │ └── ElementTransformer.kt ├── Readme.md └── api │ └── ktorfit-compiler-plugin.api ├── CHANGELOG.md ├── ktorfit-annotations ├── .gitignore ├── Readme.md ├── gradle.properties └── src │ ├── androidMain │ └── AndroidManifest.xml │ └── commonMain │ └── kotlin │ └── de │ └── jensklingenberg │ └── ktorfit │ ├── http │ ├── Multipart.kt │ ├── RequestType.kt │ ├── Url.kt │ ├── HEAD.kt │ ├── PartMap.kt │ ├── Body.kt │ ├── OPTIONS.kt │ ├── FormUrlEncoded.kt │ ├── QueryName.kt │ ├── Streaming.kt │ ├── FieldMap.kt │ ├── HeaderMap.kt │ ├── PUT.kt │ ├── GET.kt │ ├── POST.kt │ ├── PATCH.kt │ ├── Headers.kt │ ├── DELETE.kt │ ├── QueryMap.kt │ ├── ReqBuilder.kt │ ├── Part.kt │ ├── HTTP.kt │ ├── Header.kt │ ├── Field.kt │ ├── Tag.kt │ ├── Path.kt │ └── Query.kt │ └── core │ └── NoDelegation.kt ├── ktorfit-gradle-plugin ├── .gitignore ├── gradle.properties ├── Release.md └── Readme.md ├── sandbox ├── src │ ├── linuxX64Test │ │ └── kotlin │ │ │ └── MyTest.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── example │ │ │ ├── model │ │ │ ├── github │ │ │ │ ├── Issuedata.kt │ │ │ │ └── GithubFollowerResponse.kt │ │ │ ├── Envelope.kt │ │ │ ├── Post.kt │ │ │ ├── MyOwnResponse.kt │ │ │ ├── ExampleApi.kt │ │ │ ├── Specie.kt │ │ │ ├── People.kt │ │ │ ├── CommonClient.kt │ │ │ └── StringToIntRequestConverter.kt │ │ │ ├── api │ │ │ ├── Response.kt │ │ │ ├── StarWarsApi.kt │ │ │ └── GithubService.kt │ │ │ └── UserFactory.kt │ ├── linuxX64Main │ │ └── kotlin │ │ │ └── LinuxMain.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── JsMain.kt │ └── jvmMain │ │ └── kotlin │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ └── demo │ │ ├── TestApi2.kt │ │ ├── TestApi.kt │ │ ├── HeaderTestApi.kt │ │ ├── CreateIssue.kt │ │ ├── JvmPlaceHolderApi.kt │ │ └── uploadFile.txt └── Readme.md ├── ktorfit-converters ├── flow │ ├── .gitignore │ ├── gradle.properties │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── de │ │ │ └── jensklingenberg │ │ │ └── ktorfit │ │ │ └── converter │ │ │ └── FlowConverterFactory.kt │ └── api │ │ ├── android │ │ └── flow.api │ │ ├── jvm │ │ └── flow.api │ │ └── flow.klib.api ├── call │ ├── src │ │ ├── androidMain │ │ │ └── AndroidManifest.xml │ │ └── commonMain │ │ │ └── kotlin │ │ │ └── de │ │ │ └── jensklingenberg │ │ │ └── ktorfit │ │ │ ├── Call.kt │ │ │ └── Callback.kt │ └── api │ │ ├── jvm │ │ └── call.api │ │ └── android │ │ └── call.api └── response │ ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ └── converter │ │ ├── ResponseConverterFactory.kt │ │ └── ResponseClassSuspendConverter.kt │ └── api │ ├── jvm │ └── response.api │ └── android │ └── response.api ├── ktorfit-lib-core ├── gradle.properties └── src │ ├── androidMain │ └── AndroidManifest.xml │ ├── jvmMain │ └── resources │ │ └── META-INF │ │ └── proguard │ │ └── ktorfit.pro │ ├── commonMain │ └── kotlin │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ ├── internal │ │ ├── ClassProvider.kt │ │ └── InternalKtorfitApi.kt │ │ ├── Strings.kt │ │ ├── Annotations.kt │ │ ├── converter │ │ ├── KtorfitResult.kt │ │ └── builtin │ │ │ ├── DontSwallowExceptionsConverterFactory.kt │ │ │ └── DefaultSuspendResponseConverterFactory.kt │ │ └── TypeInfoExt.kt │ ├── commonTest │ └── kotlin │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ ├── TestStringToIntRequestConverter.kt │ │ ├── internal │ │ └── TypeDataTest.kt │ │ ├── converter │ │ └── RequestParameterConverterTest.kt │ │ └── TestEngine.kt │ └── jvmTest │ └── kotlin │ └── de │ └── jensklingenberg │ └── ktorfit │ └── BodyTest.kt ├── .gitignore ├── docs ├── images │ └── test │ │ └── carbon.png ├── example.json ├── manifest.webmanifest ├── android │ └── proguard.md ├── fundamentals │ └── scope.md ├── knownissues.md ├── development.md ├── converters │ ├── requestparameterconverter.md │ └── converters.md ├── assets │ └── badges │ │ └── platforms.svg ├── theme │ ├── 404.html │ └── main.html ├── configuration.md ├── quick-start.md ├── installation.md ├── suspendresponseconverter.md ├── index.md └── responseconverter.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── ktorfit-ksp ├── Readme.md ├── gradle.properties ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider │ │ └── kotlin │ │ │ └── de │ │ │ └── jensklingenberg │ │ │ └── ktorfit │ │ │ ├── model │ │ │ ├── ReturnTypeData.kt │ │ │ ├── annotations │ │ │ │ ├── HttpMethodAnnotation.kt │ │ │ │ └── FunctionAnnotation.kt │ │ │ └── KtorfitClass.kt │ │ │ ├── poetspec │ │ │ ├── ParameterSpec.kt │ │ │ └── Utils.kt │ │ │ ├── utils │ │ │ └── AnnotationSpecExt.kt │ │ │ ├── reqBuilderExtension │ │ │ ├── BodyCodeGenerator.kt │ │ │ ├── CustomRequestBuilderCodeGeneration.kt │ │ │ ├── MethodCodeGeneration.kt │ │ │ ├── RequestConverterText.kt │ │ │ ├── UrlCodeGeneration.kt │ │ │ └── AttributesCodeGenerator.kt │ │ │ ├── KtorfitOptions.kt │ │ │ └── KtorfitLogger.kt │ └── test │ │ └── kotlin │ │ └── de │ │ └── jensklingenberg │ │ └── ktorfit │ │ ├── Utils.kt │ │ ├── reqBuilderExtension │ │ ├── GetBodyDataTextKtTest.kt │ │ └── GetRequestBuilderTextKtTest.kt │ │ ├── RequestTypeTest.kt │ │ ├── HeadersAnnotationsTest.kt │ │ ├── TagAnnotationsTest.kt │ │ └── ReturnTypeDataTest.kt ├── detekt-config.yml └── detekt-baseline.xml ├── renovate.json5 ├── pyproject.toml ├── SECURITY.md ├── detekt-config.yml ├── .editorconfig ├── gradle.properties ├── RELEASING.md └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @foso 2 | -------------------------------------------------------------------------------- /ktorfit-lib/api/jvm/ktorfit-lib.api: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ktorfit-lib/api/android/ktorfit-lib.api: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ktorfit-lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | See [/docs/CHANGELOG.md](/docs/CHANGELOG.md) -------------------------------------------------------------------------------- /ktorfit-annotations/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle/ 3 | -------------------------------------------------------------------------------- /ktorfit-gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle 3 | -------------------------------------------------------------------------------- /sandbox/src/linuxX64Test/kotlin/MyTest.kt: -------------------------------------------------------------------------------- 1 | class MyTest 2 | -------------------------------------------------------------------------------- /ktorfit-converters/flow/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.gradle/ 3 | -------------------------------------------------------------------------------- /ktorfit-lib/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.androidSourceSetLayoutVersion=2 -------------------------------------------------------------------------------- /ktorfit-lib-core/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.androidSourceSetLayoutVersion=2 -------------------------------------------------------------------------------- /sandbox/Readme.md: -------------------------------------------------------------------------------- 1 | # Sandbox 2 | experimental test module to try various stuff -------------------------------------------------------------------------------- /ktorfit-annotations/Readme.md: -------------------------------------------------------------------------------- 1 | # Annotations 2 | 3 | The annotations for Ktorfit -------------------------------------------------------------------------------- /ktorfit-annotations/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.androidSourceSetLayoutVersion=2 -------------------------------------------------------------------------------- /ktorfit-converters/flow/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.mpp.androidSourceSetLayoutVersion=2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .kotlin/ 3 | .idea/ 4 | build/ 5 | **/build/ 6 | local.properties 7 | -------------------------------------------------------------------------------- /docs/images/test/carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/docs/images/test/carbon.png -------------------------------------------------------------------------------- /ktorfit-lib/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ktorfit-converters/call/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-converters/flow/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-converters/response/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/MultiplatformExample/Readme.md: -------------------------------------------------------------------------------- 1 | # KtorfitMultiplatformExample 2 | The Http Response will be printed to the logcat -------------------------------------------------------------------------------- /ktorfit-ksp/Readme.md: -------------------------------------------------------------------------------- 1 | # Ktorfit KSP 2 | 3 | The KSP plugin will generate the implementations of interfaces that are annotated -------------------------------------------------------------------------------- /docs/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "success": true, 3 | "user": { 4 | "id": 1, 5 | "name": "Jens Klingenberg" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ktorfit-ksp/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=de.jensklingenberg.ktorfit 2 | POM_NAME=ktorfit-ksp 3 | POM_ARTIFACT_ID=ktorfit-ksp 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: - 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AndroidOnlyExample 3 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | } 6 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/person/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | de.jensklingenberg.ktorfit.KtorfitProcessorProvider -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Podfile: -------------------------------------------------------------------------------- 1 | target 'iosApp' do 2 | use_frameworks! 3 | platform :ios, '14.1' 4 | pod 'shared', :path => '../shared' 5 | end -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ktorfit-gradle-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=de.jensklingenberg.ktorfit 2 | POM_NAME=gradle-plugin 3 | POM_ARTIFACT_ID=gradle-plugin 4 | POM_PACKAGING=jar 5 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ktorfit", 3 | "short_name": "Ktorfit", 4 | "description": "HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform" 5 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/MultiplatformExample/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | GROUP=de.jensklingenberg.ktorfit 2 | 3 | POM_NAME=compiler-plugin 4 | POM_ARTIFACT_ID=compiler-plugin 5 | POM_PACKAGING=jar 6 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "author": "xcode", 4 | "version": 1 5 | } 6 | } -------------------------------------------------------------------------------- /ktorfit-gradle-plugin/Release.md: -------------------------------------------------------------------------------- 1 | # Publish in Gradle plugin portal 2 | Gradle task: plugin portal> publishPlugins 3 | 4 | # Publish local 5 | :ktorfit-gradle-plugin:publishToMavenLocal -------------------------------------------------------------------------------- /ktorfit-lib-core/src/jvmMain/resources/META-INF/proguard/ktorfit.pro: -------------------------------------------------------------------------------- 1 | -keep class de.jensklingenberg.ktorfit.** { *; } 2 | -keepclassmembers class de.jensklingenberg.ktorfit.** { *; } 3 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/commonMain/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | expect class Platform() { 4 | val platform: String 5 | } -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example/MultiplatformExample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | */build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### :thinking: DOD Checklist 2 | 3 | - [ ] I did all relevant changes to the documentation and the [changelog](https://github.com/Foso/Ktorfit/blob/master/docs/CHANGELOG.md). 4 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Foso/Ktorfit/HEAD/example/AndroidOnlyExample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /ktorfit-converters/call/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Call.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | public interface Call { 4 | public fun onExecute(callBack: Callback) 5 | } 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4DADocumentation request" 3 | about: Point out what's confusing or missing 4 | title: '' 5 | labels: 'documentation' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /ktorfit-lib/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Empty.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | // This file exists because without any source file, KMP will not generate the archives for the platform targets 3 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_iosApp { 2 | umbrella header "Pods-iosApp-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/github/Issuedata.kt: -------------------------------------------------------------------------------- 1 | package com.example.model.github 2 | 3 | @kotlinx.serialization.Serializable 4 | data class Issuedata( 5 | val title: String, 6 | val body: String 7 | ) 8 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/macosX64Main/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | 4 | actual class Platform actual constructor() { 5 | actual val platform: String = "macOS" 6 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_iosApp : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_iosApp 5 | @end 6 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/jsMain/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String 5 | get() = "JS" 6 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/jvmMain/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String 5 | get() = "JVM" 6 | } -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Multipart.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Send multipart data 5 | */ 6 | @Target(AnnotationTarget.FUNCTION) 7 | annotation class Multipart 8 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "idiom": "universal" 5 | } 6 | ], 7 | "info": { 8 | "author": "xcode", 9 | "version": 1 10 | } 11 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/androidMain/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" 5 | } -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/internal/ClassProvider.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.internal 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | 5 | public interface ClassProvider { 6 | public fun create(_ktorfit: Ktorfit): T 7 | } 8 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/RequestType.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @Target(AnnotationTarget.VALUE_PARAMETER) 6 | annotation class RequestType( 7 | val requestType: KClass<*> 8 | ) 9 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Url.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * ``` 5 | * @GET 6 | * suspend fun request(@Url url: String): List 7 | * ``` 8 | */ 9 | @Target(AnnotationTarget.VALUE_PARAMETER) 10 | annotation class Url 11 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.android.library) apply false 5 | alias(libs.plugins.kotlin.android) apply false 6 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp.xcworkspace/xcuserdata/jens.klingenberg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/iosMain/kotlin/com/example/ktorfittest/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import platform.UIKit.UIDevice 4 | 5 | actual class Platform actual constructor() { 6 | actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion 7 | } -------------------------------------------------------------------------------- /example/MultiplatformExample/gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | #Kotlin 4 | kotlin.code.style=official 5 | #Android 6 | android.useAndroidX=true 7 | #MPP 8 | kotlin.mpp.enableCInteropCommonization=true 9 | kotlin.mpp.androidSourceSetLayoutVersion=2 10 | #ksp.useKSP2=true -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/Envelope.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | @kotlinx.serialization.Serializable 4 | data class Envelope( 5 | val success: Boolean, 6 | val user: User 7 | ) 8 | 9 | @kotlinx.serialization.Serializable 10 | data class User( 11 | val id: Int, 12 | val name: String 13 | ) 14 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | triage: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v6 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 15 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "recreateWhen": "never", 3 | extends: [ 4 | 'config:best-practices', 5 | ], 6 | schedule: [ 7 | 'every weekend', 8 | ], 9 | packageRules: [ 10 | { 11 | groupName: 'Ktor Client', 12 | matchPackageNames: [ 13 | '/^io\\.ktor:ktor-client(-[^:]+)?$/', 14 | ], 15 | }, 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | 4 | struct ContentView: View { 5 | let greet = Greeting().greeting() 6 | 7 | var body: some View { 8 | Text(greet) 9 | } 10 | } 11 | 12 | struct ContentView_Previews: PreviewProvider { 13 | static var previews: some View { 14 | ContentView() 15 | } 16 | } -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/HEAD.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a HEAD request. 4 | * @param value relative url path, if empty, you need to have a parameter with [Url] 5 | * */ 6 | @Target(AnnotationTarget.FUNCTION) 7 | annotation class HEAD( 8 | val value: String = "" 9 | ) 10 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/PartMap.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * If the type is List the value will be used directly with its content type. 5 | */ 6 | @Target(AnnotationTarget.VALUE_PARAMETER) 7 | annotation class PartMap( 8 | val encoding: String = "binary" 9 | ) 10 | -------------------------------------------------------------------------------- /docs/android/proguard.md: -------------------------------------------------------------------------------- 1 | If you use Ktorfit as a dependency in an Android project which uses R8 as a default compiler you don’t have to 2 | do anything. The specific rules are 3 | [already bundled](https://github.com/Foso/Ktorfit/blob/master/ktorfit-lib-core/src/jvmMain/resources/META-INF/proguard/ktorfit.pro) into the JAR 4 | which can be interpreted by R8 automatically. -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/ReturnTypeData.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.model 2 | 3 | import com.google.devtools.ksp.symbol.KSType 4 | import com.squareup.kotlinpoet.TypeName 5 | 6 | data class ReturnTypeData( 7 | val name: String, 8 | val parameterType: KSType, 9 | val typeName: TypeName? = null 10 | ) 11 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/androidMain/kotlin/com/example/ktorfittest/Test.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import de.jensklingenberg.ktorfit.http.GET 4 | import de.jensklingenberg.ktorfit.http.Path 5 | 6 | interface Test { 7 | @GET("people/{id}") 8 | suspend fun getPeopleById( 9 | @Path("id") id: Int 10 | ): Person 11 | } 12 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Body.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Use this to upload data in an HTTP Body 5 | * 6 | * ``` 7 | * @POST("createIssue") 8 | * suspend fun upload(@Body issue: Issue) 9 | * ``` 10 | */ 11 | @Target(AnnotationTarget.VALUE_PARAMETER) 12 | annotation class Body 13 | -------------------------------------------------------------------------------- /ktorfit-converters/call/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Callback.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import io.ktor.client.statement.HttpResponse 4 | 5 | public interface Callback { 6 | public fun onResponse( 7 | call: T, 8 | response: HttpResponse, 9 | ) 10 | 11 | public fun onError(exception: Throwable) 12 | } 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "Ktorfit" 3 | version = "0.1.0" 4 | description = "Documentation for Ktorfit" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "mkdocs==1.6.1", 8 | "mkdocs-material==9.5.47", 9 | "mkdocs-git-revision-date-localized-plugin==0.14.0", 10 | "mkdocs-minify-plugin==0.8.0", 11 | "mkdocs-macros-plugin==1.0.5", 12 | ] 13 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/OPTIONS.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make an OPTIONS request. 4 | * 5 | * @param value relative url path, if empty, you need to have a parameter with [Url] 6 | * */ 7 | @Target(AnnotationTarget.FUNCTION) 8 | annotation class OPTIONS( 9 | val value: String = "" 10 | ) 11 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/internal/InternalKtorfitApi.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.internal 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.WARNING, 5 | message = "This API is internal in Ktorfit and should not be used. It could be removed or changed without notice.", 6 | ) 7 | public annotation class InternalKtorfitApi 8 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - shared (1.0) 3 | 4 | DEPENDENCIES: 5 | - shared (from `../shared`) 6 | 7 | EXTERNAL SOURCES: 8 | shared: 9 | :path: "../shared" 10 | 11 | SPEC CHECKSUMS: 12 | shared: 90ed35de669e9fcb63a61e6b4bb0521eb732cc7a 13 | 14 | PODFILE CHECKSUM: f282da88f39e69507b0a255187c8a6b644477756 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - shared (1.0) 3 | 4 | DEPENDENCIES: 5 | - shared (from `../shared`) 6 | 7 | EXTERNAL SOURCES: 8 | shared: 9 | :path: "../shared" 10 | 11 | SPEC CHECKSUMS: 12 | shared: 90ed35de669e9fcb63a61e6b4bb0521eb732cc7a 13 | 14 | PODFILE CHECKSUM: f282da88f39e69507b0a255187c8a6b644477756 15 | 16 | COCOAPODS: 1.15.2 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/FormUrlEncoded.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * A function that is annotated with this, will have the header 5 | * application/x-www-form-urlencoded added to the request. 6 | * Needed to use @Field @FieldMap 7 | */ 8 | @Target(AnnotationTarget.FUNCTION) 9 | annotation class FormUrlEncoded 10 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/QueryName.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Used for query parameters 5 | * @param encoded true means that this value is already URL encoded and will not be encoded again 6 | */ 7 | @Target(AnnotationTarget.VALUE_PARAMETER) 8 | annotation class QueryName( 9 | val encoded: Boolean = false 10 | ) 11 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Streaming.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * 5 | * ``` 6 | * @Streaming 7 | * @GET("posts") 8 | * suspend fun getPostsStreaming(): HttpStatement 9 | * ``` 10 | * 11 | * The return type has to be HttpStatement 12 | */ 13 | @Target(AnnotationTarget.FUNCTION) 14 | annotation class Streaming 15 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /example/MultiplatformExample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/FieldMap.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Needs to be used in combination with [FormUrlEncoded] 5 | * @param encoded true means that this value is already URL encoded and will not be encoded again 6 | */ 7 | @Target(AnnotationTarget.VALUE_PARAMETER) 8 | annotation class FieldMap( 9 | val encoded: Boolean = false 10 | ) 11 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/api/Response.kt: -------------------------------------------------------------------------------- 1 | package com.example.api 2 | 3 | sealed class Response { 4 | data class Success( 5 | val data: T 6 | ) : Response() 7 | 8 | class Error( 9 | val ex: Throwable 10 | ) : Response() 11 | 12 | companion object { 13 | fun success(data: T) = Success(data) 14 | 15 | fun error(ex: Throwable) = Error(ex) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/HeaderMap.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Add headers to a request 5 | * 6 | * ``` 7 | * @GET("comments") 8 | * suspend fun requestWithHeaders(@HeaderMap headerMap : Map): List 9 | * ``` 10 | * 11 | * @see Headers 12 | * @see Header 13 | */ 14 | @Target(AnnotationTarget.VALUE_PARAMETER) 15 | annotation class HeaderMap 16 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/PUT.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a PUT request. 4 | * ``` 5 | * @PUT("putIssue") 6 | * suspend fun putIssue(@Body issue: Issue) 7 | * ``` 8 | * @param value relative url path, if empty, you need to have a parameter with [Url] 9 | * 10 | */ 11 | @Target(AnnotationTarget.FUNCTION) 12 | annotation class PUT( 13 | val value: String = "" 14 | ) 15 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/GET.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a GET request. 4 | * ``` 5 | * @GET("issue") 6 | * suspend fun getIssue(@Query("id") id: String) : Issue 7 | * ``` 8 | * @param value relative url path, if empty, you need to have a parameter with [Url] 9 | * */ 10 | @Target(AnnotationTarget.FUNCTION) 11 | annotation class GET( 12 | val value: String = "" 13 | ) 14 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/POST.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a POST request. 4 | * ``` 5 | * @POST("issue") 6 | * suspend fun postIssue(@Body issue: Issue) 7 | * ``` 8 | * @param value relative url path, if empty, you need to have a parameter with [Url] 9 | * 10 | */ 11 | @Target(AnnotationTarget.FUNCTION) 12 | annotation class POST( 13 | val value: String = "" 14 | ) 15 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/PATCH.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a PATCH request. 4 | * ``` 5 | * @PATCH("issue") 6 | * suspend fun patchIssue(@Body issue: Issue) 7 | * ``` 8 | * @param value relative url path, if empty, you need to have a parameter with [Url] 9 | * 10 | */ 11 | @Target(AnnotationTarget.FUNCTION) 12 | annotation class PATCH( 13 | val value: String = "" 14 | ) 15 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/ParameterSpec.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.poetspec 2 | 3 | import com.squareup.kotlinpoet.ParameterSpec 4 | import de.jensklingenberg.ktorfit.model.ParameterData 5 | 6 | fun ParameterData.parameterSpec(): ParameterSpec { 7 | val parameterType = this.type.typeName ?: throw IllegalStateException("Type ${this.name} not found") 8 | return ParameterSpec(this.name, parameterType) 9 | } 10 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/Post.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Post( 7 | val userId: Int, 8 | val id: Int, 9 | val title: String, 10 | val body: String 11 | ) 12 | 13 | @Serializable 14 | data class Comment( 15 | val postId: Int, 16 | val id: Int, 17 | val name: String, 18 | val body: String, 19 | val email: String 20 | ) 21 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/GitHubService.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample 2 | 3 | import de.jensklingenberg.ktorfit.http.GET 4 | import de.jensklingenberg.ktorfit.http.Path 5 | 6 | interface GitHubService { 7 | @GET("repos/{user}/{repo}/releases/latest") 8 | suspend fun getLatestRelease( 9 | @Path("user") user: String, 10 | @Path("repo") repo: String, 11 | ): String 12 | } -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /ktorfit-gradle-plugin/Readme.md: -------------------------------------------------------------------------------- 1 | # Gradle plugin 2 | The Gradle plugin is needed to easily add the compiler plugin to the project and pass data to it. 3 | 4 | Add this plugin from Gradle plugin portal: 5 | 6 | ```kotlin 7 | plugins { 8 | id "de.jensklingenberg.ktorfit" version "LATEST_VERSION" 9 | } 10 | ``` 11 | 12 | The plugin can be configured: 13 | 14 | ```kotlin 15 | configure { 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_iosAppVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_iosAppVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /example/MultiplatformExample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | google() 5 | gradlePluginPortal() 6 | mavenCentral() 7 | 8 | maven { 9 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 10 | } 11 | } 12 | } 13 | 14 | rootProject.name = "KtorfitMultiplatformExample" 15 | include(":androidApp") 16 | include(":shared") 17 | include(":person") 18 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Headers.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Add headers to a request 5 | * 6 | * ``` 7 | * @Headers("Accept: application/json","Content-Type: application/json") 8 | * @GET("comments") 9 | * suspend fun requestWithHeaders(): List 10 | * ``` 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | annotation class Headers( 14 | vararg val value: String 15 | ) 16 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/annotations/HttpMethodAnnotation.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.model.annotations 2 | 3 | open class HttpMethodAnnotation( 4 | open val path: String, 5 | open val httpMethod: HttpMethod, 6 | ) : FunctionAnnotation() 7 | 8 | class CustomHttp( 9 | override val path: String, 10 | override val httpMethod: HttpMethod, 11 | val customValue: String, 12 | ) : HttpMethodAnnotation(path, httpMethod) 13 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/DELETE.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a DELETE request. 4 | * 5 | * ``` 6 | * @DELETE("deleteIssue") 7 | * suspend fun deleteIssue(@Query("id") id: String) 8 | * ``` 9 | * @param value relative url path, if empty, you need to have a parameter with [Url] 10 | * 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | annotation class DELETE( 14 | val value: String = "" 15 | ) 16 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/QueryMap.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Used for query parameters 5 | * 6 | * *

A {@code null} value for the map, as a key is not allowed. 7 | * @param encoded true means that this value is already URL encoded and will not be encoded again 8 | 9 | */ 10 | @Target(AnnotationTarget.VALUE_PARAMETER) 11 | annotation class QueryMap( 12 | val encoded: Boolean = false 13 | ) 14 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/utils/AnnotationSpecExt.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.utils 2 | 3 | import com.squareup.kotlinpoet.AnnotationSpec 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.ParameterizedTypeName 6 | 7 | fun AnnotationSpec.toClassName(): ClassName { 8 | return if (typeName is ClassName) { 9 | typeName as ClassName 10 | } else { 11 | (typeName as ParameterizedTypeName).rawType 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp.xcodeproj/xcuserdata/jens.klingenberg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iosApp.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/ReqBuilder.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * This can be used to add additional configurations to a request 5 | * the parameter type has to be HttpRequestBuilder.() -> Unit 6 | * 7 | * ``` 8 | * @GET("posts") 9 | * suspend fun getPosts(@ReqBuilder builder : HttpRequestBuilder.() -> Unit) : List 10 | * ``` 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | annotation class ReqBuilder 14 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/commonTest/kotlin/com/example/ktorfittest/TestApi.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import de.jensklingenberg.ktorfit.http.GET 4 | import de.jensklingenberg.ktorfit.http.Path 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface TestApi { 8 | companion object { 9 | const val baseUrl = "https://swapi.dev/api/" 10 | } 11 | 12 | @GET("people/{id}/") 13 | suspend fun getPersonByIdResponse(@Path("id") peopleId: Int): Flow 14 | } 15 | 16 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/jvmMain/kotlin/com/example/ktorfittest/StarWarsApi.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import de.jensklingenberg.ktorfit.http.GET 4 | import de.jensklingenberg.ktorfit.http.Path 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface JvmStarWarsApi { 8 | @GET("people/{id}/") 9 | suspend fun getPersonByIdResponse(@Path("id") peopleId: Int): String 10 | 11 | @GET("people/{id}/") 12 | fun getPersonByIdFlowResponse(@Path("id") peopleId: Int): Flow 13 | } -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Strings.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | internal class Strings { 4 | companion object { 5 | const val EXPECTED_URL_SCHEME = "Expected URL scheme 'http' or 'https' was not found" 6 | const val BASE_URL_REQUIRED = "Base URL required" 7 | const val ENABLE_GRADLE_PLUGIN = "You need to enable the Ktorfit Gradle Plugin" 8 | const val BASE_URL_NEEDS_TO_END_WITH = "Base URL needs to end with /" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/test/java/de/jensklingenberg/androidonlyexample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/BodyCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import de.jensklingenberg.ktorfit.model.ParameterData 4 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Body 5 | 6 | fun getBodyDataText(params: List): String = 7 | params 8 | .firstOrNull { 9 | it.hasAnnotation() 10 | }?.name 11 | ?.let { "setBody($it)" } 12 | .orEmpty() 13 | -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/src/main/java/de/jensklingenberg/ktorfit/DebugLogger.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 4 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 5 | 6 | internal data class DebugLogger(val debug: Boolean, val messageCollector: MessageCollector) { 7 | fun log(message: String) { 8 | if (debug) { 9 | messageCollector.report(CompilerMessageSeverity.INFO, message) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/i-d-like-to-request-a-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: I'd like to request a feature about: Suggest a new idea for this plugin. title: '' 3 | labels: enhancement assignees: '' 4 | 5 | --- 6 | 7 | **Describe the use-cases of your feature** 8 | 9 | 10 | 11 | **Describe your solution** 12 | 13 | 14 | -------------------------------------------------------------------------------- /ktorfit-lib/api/ktorfit-lib.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Part.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * String OR List< PartData> 5 | * ``` 6 | * @Multipart 7 | * @POST("upload") 8 | * suspend fun uploadFile(@Part("description") description: String, @Part("description") data: List): String 9 | * ``` 10 | * @param value part name 11 | * Part parameters type may not be nullable. 12 | */ 13 | @Target(AnnotationTarget.VALUE_PARAMETER) 14 | annotation class Part( 15 | val value: String = "" 16 | ) 17 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/MyOwnResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | sealed class MyOwnResponse { 7 | data class Success( 8 | val data: T 9 | ) : MyOwnResponse() 10 | 11 | class Error( 12 | val ex: Throwable 13 | ) : MyOwnResponse() 14 | 15 | companion object { 16 | fun success(data: T) = Success(data) 17 | 18 | fun error(ex: Throwable) = Error(ex) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Install JDK 21 15 | uses: actions/setup-java@v5 16 | with: 17 | distribution: 'temurin' 18 | java-version: 21 19 | - uses: actions/checkout@v6 20 | 21 | - name: Update Gradle Wrapper 22 | uses: gradle-update/update-gradle-wrapper-action@v2 -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/jsMain/kotlin/com/example/ktorfittest/main.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import kotlinx.coroutines.GlobalScope 4 | import kotlinx.coroutines.delay 5 | import kotlinx.coroutines.launch 6 | 7 | //Run with jsNodeRun 8 | fun main() { 9 | GlobalScope.launch { 10 | 11 | 12 | println("Launch") 13 | 14 | 15 | starWarsApi.getPeopleByIdFlowResponse(3,null).collect { 16 | println("JS getPeopleByIdFlowResponse:" + it.name) 17 | } 18 | 19 | delay(3000) 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/CustomRequestBuilderCodeGeneration.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import de.jensklingenberg.ktorfit.model.ParameterData 4 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation 5 | 6 | fun getCustomRequestBuilderText(parameterDataList: List): String = 7 | parameterDataList 8 | .find { it.hasAnnotation() } 9 | ?.let { 10 | it.name + "(this)" 11 | }.orEmpty() 12 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/HTTP.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** Make a request with a custom HTTP method. 4 | * ``` 5 | * @HTTP(method = "CUSTOM", path = "custom/endpoint/") 6 | * suspend fun getIssue(@Query("id") id: String) : Issue 7 | * ``` 8 | * @param method HTTP method verb. 9 | * @param path URL path. 10 | * @param hasBody 11 | * */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | annotation class HTTP( 14 | val method: String, 15 | val path: String = "", 16 | val hasBody: Boolean = false 17 | ) 18 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/commonTest/kotlin/com/example/ktorfittest/KtorfitTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import kotlinx.coroutines.GlobalScope 4 | import kotlinx.coroutines.launch 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class KtorfitTest { 9 | @Test 10 | fun test() { 11 | GlobalScope.launch { 12 | ktorfit.create() 13 | .getPersonByIdResponse(3) 14 | .collect { 15 | assertEquals(it.name, "R2-D2") 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/ExampleApi.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | import de.jensklingenberg.ktorfit.Response 4 | import de.jensklingenberg.ktorfit.http.GET 5 | import de.jensklingenberg.ktorfit.http.Headers 6 | 7 | interface ExampleApi { 8 | 9 | @Headers("User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0") 10 | @GET("example.json") 11 | suspend fun getUser(): Response 12 | 13 | @GET("example.json") 14 | suspend fun getUserResponse(): MyOwnResponse 15 | } 16 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/core/NoDelegation.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.core 2 | 3 | /** 4 | * Indicates that the annotated interface should not be delegated in the generated implementation. 5 | * 6 | * When an interface is annotated with @NoDelegation, the generated implementation will not use 7 | * Kotlin delegation for this interface. This is useful when you want to manually implement the 8 | * methods of the interface or when delegation is not desired for other reasons. 9 | */ 10 | @Target(AnnotationTarget.TYPE) 11 | annotation class NoDelegation 12 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Header.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Add a header to a request 5 | * 6 | * ``` 7 | * @GET("comments") 8 | * suspend fun request( @Header("Content-Type") name: String): List 9 | * ``` 10 | * 11 | * A request with request("Hello World") will have the header "Content-Type:Hello World" 12 | * header with null values will be ignored 13 | * @see Headers 14 | * @see HeaderMap 15 | */ 16 | @Target(AnnotationTarget.VALUE_PARAMETER) 17 | annotation class Header( 18 | val value: String 19 | ) 20 | -------------------------------------------------------------------------------- /docs/fundamentals/scope.md: -------------------------------------------------------------------------------- 1 | # Scope of Ktorfit 2 | 3 | The goal of Ktorfit is to provide a similar developer experience like [Retrofit](https://square.github.io/retrofit/) for Kotlin Multiplatform projects. It`s not a 100% drop-in replacement for Retrofit. It uses [Ktor clients](https://ktor.io/docs/getting-started-ktor-client.html) because they are available on nearly every compile target of KMP. 4 | Every feature should be implemented so that it works on all platforms that Ktor supports. Before a new functionality is added to Ktorfit, it should be checked if there is already a Ktor plugin for it which solves the same problem. -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Field.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Needs to be used in combination with [FormUrlEncoded] 5 | * @param value The default value will be replaced with the name of the parameter that is annotated. 6 | * @param encoded true means that this value is already URL encoded and will not be encoded again 7 | * @see FormUrlEncoded 8 | * @see FieldMap 9 | */ 10 | @Target(AnnotationTarget.VALUE_PARAMETER) 11 | annotation class Field( 12 | val value: String = "KTORFIT_DEFAULT_VALUE", 13 | val encoded: Boolean = false 14 | ) 15 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/MethodCodeGeneration.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import de.jensklingenberg.ktorfit.model.annotations.CustomHttp 4 | import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation 5 | 6 | fun getMethodCode(httpMethod: HttpMethodAnnotation): String { 7 | val httpMethodValue = 8 | if (httpMethod is CustomHttp) { 9 | httpMethod.customValue 10 | } else { 11 | httpMethod.httpMethod.keyword 12 | } 13 | return "this.method = HttpMethod.parse(\"${httpMethodValue}\")" 14 | } 15 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/Annotations.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import io.ktor.client.request.HttpRequest 4 | import io.ktor.client.request.HttpRequestBuilder 5 | import io.ktor.util.AttributeKey 6 | 7 | public val annotationsAttributeKey: AttributeKey> = AttributeKey("__ktorfit_attribute_annotations") 8 | 9 | public val HttpRequest.annotations: List 10 | inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() 11 | 12 | public val HttpRequestBuilder.annotations: List 13 | inline get() = attributes.getOrNull(annotationsAttributeKey) ?: emptyList() 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Versions currently being supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | latest | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please use an [Issue](https://github.com/Foso/Ktorfit/security/advisories/new) to report vulnerabilities. 14 | 15 | If security bug is discovered, following actions will be taken: 16 | 17 | - Confirm the problem and determine the affected versions. 18 | - Audit code to find any potential similar problems. 19 | - Prepare fixes for all releases still under maintenance. 20 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Pods.xcodeproj/xcuserdata/jensklingenberg.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Pods-iosApp.xcscheme 8 | 9 | isShown 10 | 11 | 12 | shared.xcscheme 13 | 14 | isShown 15 | 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/MultiplatformExample/person/src/commonMain/kotlin/com/example/ktorfittest/Person.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | @kotlinx.serialization.Serializable 4 | data class Person( 5 | val films: List? = null, 6 | val homeworld: String? = null, 7 | val gender: String? = null, 8 | val skinColor: String? = null, 9 | val edited: String? = null, 10 | val created: String? = null, 11 | val mass: String? = null, 12 | val url: String? = null, 13 | val hairColor: String? = null, 14 | val birthYear: String? = null, 15 | val eyeColor: String? = null, 16 | val name: String? = null, 17 | val height: String? = null 18 | ) 19 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Tag.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Adds the argument instance as a request tag using the type as a AttributeKey. 5 | * 6 | * ``` 7 | * @GET("/") 8 | * fun foo(@Tag tag: String) 9 | * ``` 10 | * 11 | * Tag arguments may be `null` which will omit them from the request. 12 | * 13 | * @param value Will be used as the name for the attribute key. The default value will be replaced with the name of the parameter that is annotated. 14 | * 15 | */ 16 | @Target(AnnotationTarget.VALUE_PARAMETER) 17 | annotation class Tag( 18 | val value: String = "KTORFIT_DEFAULT_VALUE" 19 | ) 20 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Updates for Github Actions used in the repo 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | GitHub_Actions: 13 | patterns: 14 | - "*" 15 | # Updates for Gradle dependencies used in the app 16 | - package-ecosystem: gradle 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | day: monday 21 | time: "12:00" 22 | target-branch: master 23 | -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/src/main/java/de/jensklingenberg/ktorfit/KtorfitIrGenerationExtension.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.ir.declarations.IrModuleFragment 6 | 7 | internal class KtorfitIrGenerationExtension(private val debugLogger: DebugLogger) : IrGenerationExtension { 8 | override fun generate( 9 | moduleFragment: IrModuleFragment, 10 | pluginContext: IrPluginContext, 11 | ) { 12 | moduleFragment.transform(ElementTransformer(pluginContext, debugLogger), null) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/annotations/FunctionAnnotation.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.model.annotations 2 | 3 | enum class HttpMethod( 4 | val keyword: String 5 | ) { 6 | GET("GET"), 7 | POST("POST"), 8 | PUT("PUT"), 9 | DELETE("DELETE"), 10 | HEAD("HEAD"), 11 | PATCH("PATCH"), 12 | CUSTOM(""), 13 | } 14 | 15 | /** 16 | * Annotation at a function 17 | */ 18 | open class FunctionAnnotation 19 | 20 | class Headers( 21 | val value: List 22 | ) : FunctionAnnotation() 23 | 24 | object FormUrlEncoded : FunctionAnnotation() 25 | 26 | object Streaming : FunctionAnnotation() 27 | 28 | object Multipart : FunctionAnnotation() 29 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/src/main/java/com/example/myapplication/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.myapplication.android 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | 6 | import android.widget.TextView 7 | import com.example.ktorfittest.Greeting 8 | 9 | fun greet(): String { 10 | return Greeting().greeting() 11 | } 12 | 13 | class MainActivity : AppCompatActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_main) 17 | 18 | val tv: TextView = findViewById(R.id.text_view) 19 | tv.text = greet() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/Person.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Person( 7 | val films: List? = null, 8 | val homeworld: String? = null, 9 | val gender: String? = null, 10 | val skinColor: String? = null, 11 | val edited: String? = null, 12 | val created: String? = null, 13 | val mass: String? = null, 14 | val url: String? = null, 15 | val hairColor: String? = null, 16 | val birthYear: String? = null, 17 | val eyeColor: String? = null, 18 | val name: String? = null, 19 | val height: String? = null 20 | ) -------------------------------------------------------------------------------- /example/AndroidOnlyExample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenLocal() 6 | mavenCentral() 7 | maven { 8 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 9 | } 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | google() 16 | mavenLocal() 17 | mavenCentral() 18 | maven { 19 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 20 | } 21 | } 22 | } 23 | rootProject.name = "AndroidOnlyExample" 24 | include(":app") -------------------------------------------------------------------------------- /ktorfit-converters/flow/api/android/flow.api: -------------------------------------------------------------------------------- 1 | public final class de/jensklingenberg/ktorfit/converter/FlowConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 2 | public fun ()V 3 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 4 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 5 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /ktorfit-converters/flow/api/jvm/flow.api: -------------------------------------------------------------------------------- 1 | public final class de/jensklingenberg/ktorfit/converter/FlowConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 2 | public fun ()V 3 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 4 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 5 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/Specie.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | @kotlinx.serialization.Serializable 4 | data class Specie( 5 | val films: List? = null, 6 | val skinColors: String? = null, 7 | val homeworld: String? = null, 8 | val edited: String? = null, 9 | val created: String? = null, 10 | val eyeColors: String? = null, 11 | val language: String? = null, 12 | val classification: String? = null, 13 | val people: List? = null, 14 | val url: String? = null, 15 | val hairColors: String? = null, 16 | val averageHeight: String? = null, 17 | val name: String? = null, 18 | val designation: String? = null, 19 | val averageLifespan: String? = null 20 | ) 21 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/api/StarWarsApi.kt: -------------------------------------------------------------------------------- 1 | package com.example.api 2 | 3 | import com.example.model.People 4 | import de.jensklingenberg.ktorfit.Call 5 | import de.jensklingenberg.ktorfit.http.GET 6 | import de.jensklingenberg.ktorfit.http.Path 7 | import kotlinx.coroutines.flow.Flow 8 | 9 | interface StarWarsApi { 10 | companion object { 11 | const val baseUrl = "https://swapi.dev/api/" 12 | } 13 | 14 | @GET("people/{id}/") 15 | fun getPersonById( 16 | @Path("id") peopleId: Int 17 | ): Call 18 | 19 | @GET("people/stormtrooper/all") 20 | fun subscribeToStormtroopers(): Flow> 21 | 22 | @GET("people/stormtrooper/all") 23 | suspend fun summonStormtroopers(): List 24 | } 25 | -------------------------------------------------------------------------------- /sandbox/src/linuxX64Main/kotlin/LinuxMain.kt: -------------------------------------------------------------------------------- 1 | import com.example.api.JsonPlaceHolderApi 2 | import de.jensklingenberg.ktorfit.Ktorfit 3 | import de.jensklingenberg.ktorfit.converter.FlowConverterFactory 4 | import io.ktor.client.HttpClient 5 | import kotlinx.coroutines.runBlocking 6 | 7 | fun main() { 8 | val linuxKtorfit = 9 | Ktorfit 10 | .Builder() 11 | .baseUrl(JsonPlaceHolderApi.baseUrl) 12 | .httpClient(HttpClient()) 13 | .converterFactories(FlowConverterFactory()) 14 | .build() 15 | 16 | val api = linuxKtorfit.create() 17 | runBlocking { 18 | api.getPosts().collect { 19 | println(it) 20 | } 21 | } 22 | 23 | println("ddd") 24 | } 25 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/KtorfitResult.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter 2 | 3 | import io.ktor.client.statement.HttpResponse 4 | 5 | /** 6 | * Represents the result from a Ktorfit request. */ 7 | public sealed interface KtorfitResult { 8 | /** 9 | * Represents a successful response. 10 | * @property response The HTTP response. 11 | */ 12 | public class Success( 13 | public val response: HttpResponse 14 | ) : KtorfitResult 15 | 16 | /** 17 | * Represents a failed response. 18 | * @property throwable The throwable associated with the failure. 19 | */ 20 | public class Failure( 21 | public val throwable: Throwable 22 | ) : KtorfitResult 23 | } 24 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/People.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | @kotlinx.serialization.Serializable 4 | data class People( 5 | val films: List? = null, 6 | val homeworld: String? = null, 7 | val gender: String? = null, 8 | val skinColor: String? = null, 9 | val edited: String? = null, 10 | val created: String? = null, 11 | val mass: String? = null, 12 | // val vehicles: List? = null, 13 | val url: String? = null, 14 | val hairColor: String? = null, 15 | val birthYear: String? = null, 16 | val eyeColor: String? = null, 17 | // val species: List? = null, 18 | // val starships: List? = null, 19 | val name: String? = null, 20 | val height: String? = null 21 | ) 22 | -------------------------------------------------------------------------------- /ktorfit-converters/response/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/ResponseConverterFactory.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import de.jensklingenberg.ktorfit.Response 5 | import io.ktor.client.statement.HttpResponse 6 | 7 | /** 8 | * Converter for [Response] 9 | */ 10 | public class ResponseConverterFactory : Converter.Factory { 11 | override fun suspendResponseConverter( 12 | typeData: TypeData, 13 | ktorfit: Ktorfit, 14 | ): Converter.SuspendResponseConverter? { 15 | if (typeData.typeInfo.type == Response::class) { 16 | return ResponseClassSuspendConverter(typeData, ktorfit) 17 | } 18 | return null 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/TypeInfoExt.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import io.ktor.util.reflect.TypeInfo 4 | import kotlin.reflect.KClass 5 | 6 | /** 7 | * This will return the upper bound type. 8 | * 9 | * Example: Response will return String as TypeInfo with upperBoundType(0) 10 | */ 11 | public fun TypeInfo.upperBoundType(index: Int = 0): TypeInfo? { 12 | val parentType = this.kotlinType ?: return null 13 | val modelKTypeProjection = if (parentType.arguments.isNotEmpty()) parentType.arguments[index] else return null 14 | val modelKType = modelKTypeProjection.type ?: return null 15 | val modelClass = (modelKType.classifier as? KClass<*>?) ?: return null 16 | return TypeInfo(modelClass, modelKType) 17 | } 18 | -------------------------------------------------------------------------------- /docs/knownissues.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | ## KMP project with single target 4 | 5 | * Unresolved reference for API class 6 | 7 | When you have a KMP project with a single target, IntelliJ will find the generated "create" extension function (e.g. 8 | **ktorfit.createExampleApi()**) in your common module, but the compilation will fail 9 | because of an "Unresolved reference" error. In that case, you have to use **ktorfit.create<ExampleApi>()** to make it work, even though it's already deprecated. 10 | 11 | Kotlin handles the compilation of a KMP project with a single target differently than with multiple targets. 12 | 13 | See: 14 | 15 | * https://youtrack.jetbrains.com/issue/KT-59129 16 | 17 | * https://youtrack.jetbrains.com/issue/KT-52664/Multiplatform-projects-with-a-single-target -------------------------------------------------------------------------------- /detekt-config.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | weights: 4 | # complexity: 2 5 | # LongParameterList: 1 6 | # style: 1 7 | # comments: 1 8 | 9 | complexity: 10 | active: true 11 | ComplexMethod: 12 | active: true 13 | threshold: 16 14 | ComplexCondition: 15 | active: true 16 | threshold: 5 17 | LongMethod: 18 | active: true 19 | excludes: ['**/test/**', '**/androidTest/**'] 20 | LongParameterList: 21 | active: true 22 | ignoreDefaultParameters: true 23 | TooManyFunctions: 24 | active: true 25 | ignoreOverridden: true 26 | 27 | style: 28 | ReturnCount: 29 | active: true 30 | max: 5 31 | excludedFunctions: "equals" 32 | excludeLabeled: false 33 | excludeReturnFromLambda: true 34 | UnusedImports: 35 | active: true 36 | -------------------------------------------------------------------------------- /ktorfit-ksp/detekt-config.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | weights: 4 | # complexity: 2 5 | # LongParameterList: 1 6 | # style: 1 7 | # comments: 1 8 | 9 | complexity: 10 | active: true 11 | ComplexMethod: 12 | active: true 13 | threshold: 16 14 | ComplexCondition: 15 | active: true 16 | threshold: 5 17 | LongMethod: 18 | active: true 19 | excludes: [ '**/test/**', '**/androidTest/**' ] 20 | LongParameterList: 21 | active: true 22 | ignoreDefaultParameters: true 23 | TooManyFunctions: 24 | active: true 25 | ignoreOverridden: true 26 | 27 | style: 28 | ReturnCount: 29 | active: true 30 | max: 5 31 | excludedFunctions: "equals" 32 | excludeLabeled: false 33 | excludeReturnFromLambda: true 34 | UnusedImports: 35 | active: true 36 | -------------------------------------------------------------------------------- /sandbox/src/jsMain/kotlin/JsMain.kt: -------------------------------------------------------------------------------- 1 | import com.example.model.Comment 2 | import com.example.model.MyOwnResponse 3 | import com.example.model.jsonPlaceHolderApi 4 | import kotlinx.coroutines.GlobalScope 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.launch 7 | 8 | fun main() { 9 | GlobalScope.launch { 10 | println("Launch") 11 | 12 | when (val test = jsonPlaceHolderApi.getCommentsByPostIdResponse("3")) { 13 | is MyOwnResponse.Success -> { 14 | val list = test.data as List 15 | println(list.size) 16 | } 17 | 18 | else -> { 19 | val error = (test as MyOwnResponse.Error<*>) 20 | println(error.ex) 21 | } 22 | } 23 | 24 | delay(3000) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/MultiplatformExample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | // mavenLocal() 4 | gradlePluginPortal() 5 | google() 6 | mavenCentral() 7 | maven { 8 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 9 | } 10 | } 11 | dependencies { 12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0") 13 | classpath("com.android.tools.build:gradle:8.12.0") 14 | classpath("org.jetbrains.kotlin:kotlin-serialization:2.2.0") 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | // mavenLocal() 21 | google() 22 | mavenCentral() 23 | maven { 24 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/Readme.md: -------------------------------------------------------------------------------- 1 | # Compiler plugin 2 | The compiler plugin transform the usage of the create function from Ktorfit-lib 3 | 4 | It looks for the every usage of the create function from the Ktorfit-lib and adds an object of the 5 | wanted implementation class as an argument. Because of the naming convention of the generated classes 6 | we can deduce the name of the class from the name of type parameter. 7 | 8 | ```kotlin 9 | val api = jvmKtorfit.create() 10 | ``` 11 | 12 | will be transformed to: 13 | 14 | ```kotlin 15 | val api = jvmKtorfit.create(_ExampleApiImpl(jvmKtorfit)) 16 | ``` 17 | 18 | # Compatibility table 19 | | Compiler plugin version | Kotlin | 20 | |-------------------------|-----------| 21 | | 2.3.2 | 2.2.21 | 22 | | 2.3.3 | 2.3.0-RC3 | 23 | 24 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | ENABLE_USER_SCRIPT_SANDBOXING = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -l"c++" -framework "shared" 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /.github/workflows/publish-gradle-plugin.yml: -------------------------------------------------------------------------------- 1 | name: Publish Gradle Plugin 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-gradle-plugin: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v6 12 | - name: Set up JDK 21 13 | uses: actions/setup-java@v5 14 | with: 15 | distribution: 'temurin' 16 | java-version: 21 17 | - name: Set up Gradle 18 | uses: gradle/actions/setup-gradle@v5 19 | - name: Publish plugin 20 | env: 21 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 22 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 23 | run: ./gradlew :ktorfit-gradle-plugin:publishPlugins -Dgradle.publish.key=${GRADLE_PUBLISH_KEY} -Dgradle.publish.secret=${GRADLE_PUBLISH_SECRET} -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | ENABLE_USER_SCRIPT_SANDBOXING = NO 3 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" 4 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -l"c++" -framework "shared" 7 | PODS_BUILD_DIR = ${BUILD_DIR} 8 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 9 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 10 | PODS_ROOT = ${SRCROOT}/Pods 11 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/jvmMain/kotlin/JvmExampleClass.kt: -------------------------------------------------------------------------------- 1 | import com.example.ktorfittest.Person 2 | import com.example.ktorfittest.starWarsApi 3 | import de.jensklingenberg.ktorfit.Callback 4 | import io.ktor.client.statement.* 5 | import kotlinx.coroutines.runBlocking 6 | 7 | fun main() { 8 | starWarsApi.getPeopleByIdCallResponse(3).onExecute( 9 | object : Callback { 10 | override fun onError(exception: Throwable) { 11 | } 12 | 13 | override fun onResponse( 14 | call: Person, 15 | response: HttpResponse 16 | ) { 17 | println("onResponse" + call) 18 | } 19 | } 20 | ) 21 | 22 | runBlocking { 23 | val response = starWarsApi.getPersonByIdResponse(3) 24 | println(response) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | ij_kotlin_name_count_to_use_star_import = 2147483647 5 | 6 | [*.{kt,kts}] 7 | # Disabled rules: 8 | # noinspection EditorConfigKeyCorrectness 9 | ktlint_standard_trailing-comma-on-call-site = disabled 10 | # noinspection EditorConfigKeyCorrectness 11 | ktlint_standard_trailing-comma-on-declaration-site = disabled 12 | # noinspection EditorConfigKeyCorrectness 13 | ktlint_standard_import-ordering = disabled 14 | # noinspection EditorConfigKeyCorrectness 15 | ktlint_standard_multiline-if-else = disabled 16 | # noinspection EditorConfigKeyCorrectness 17 | ktlint_standard_comment-wrapping = disabled 18 | # noinspection EditorConfigKeyCorrectness 19 | ktlint_standard_block-comment-initial-star-alignment = disabled 20 | # noinspection EditorConfigKeyCorrectness 21 | ktlint_standard_no-single-line-block-comment = disabled 22 | max_line_length=off -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | build: 2 | - changed-files: 3 | - any-glob-to-any-file: '**/*.gradle' 4 | - any-glob-to-any-file: '**/*.gradle.kts' 5 | 6 | ci: 7 | - changed-files: 8 | - any-glob-to-any-file: '.github/*' 9 | 10 | compiler-plugin: 11 | - changed-files: 12 | - any-glob-to-any-file: 'compiler-plugin/src/**/*' 13 | 14 | documentation: 15 | - changed-files: 16 | - any-glob-to-any-file: 'docs/*.md' 17 | 18 | ktorfit-gradle-plugin: 19 | - changed-files: 20 | - any-glob-to-any-file: 'ktorfit-gradle-plugin/src/**/*' 21 | 22 | ktorfit-ksp: 23 | - changed-files: 24 | - any-glob-to-any-file: 'ktorfit-ksp/src/**/*' 25 | 26 | ktorfit-lib: 27 | - changed-files: 28 | - any-glob-to-any-file: 'ktorfit-lib/src/**/*' 29 | - any-glob-to-any-file: 'ktorfit-lib-core/src/**/*' 30 | 31 | sandbox: 32 | - changed-files: 33 | - any-glob-to-any-file: 'sandbox/src/**/*' 34 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitOptions.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | class KtorfitOptions( 4 | options: Map 5 | ) { 6 | /** 7 | * 0: Turn off all Ktorfit related error checking 8 | * 9 | * 1: Check for errors 10 | * 11 | * 2: Turn errors into warnings 12 | */ 13 | val errorsLoggingType: Int = (options["Ktorfit_Errors"]?.toIntOrNull()) ?: 1 14 | 15 | /** 16 | * If set to true, the generated code will contain qualified type names 17 | */ 18 | val setQualifiedType = options["Ktorfit_QualifiedTypeName"]?.toBoolean() ?: false 19 | 20 | /** 21 | * If the compilation is multiplatform and has only one target, this will be true 22 | */ 23 | val multiplatformWithSingleTarget = options["Ktorfit_MultiplatformWithSingleTarget"]?.toBoolean() ?: false 24 | } 25 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/androidTest/java/de/jensklingenberg/androidonlyexample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("de.jensklingenberg.androidonlyexample", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/TestJava.java: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample; 2 | 3 | import static de.jensklingenberg.androidonlyexample.MainActivityKt.getApi; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import de.jensklingenberg.ktorfit.Callback; 9 | import io.ktor.client.statement.HttpResponse; 10 | 11 | public class TestJava { 12 | 13 | void test() { 14 | getApi().getPersonCall(1).onExecute(new Callback() { 15 | 16 | @Override 17 | public void onError(@NonNull Throwable throwable) { 18 | 19 | } 20 | 21 | @Override 22 | public void onResponse(Person person, @NonNull HttpResponse httpResponse) { 23 | Log.d("Android:", person.toString()); 24 | } 25 | 26 | }); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/github/GithubFollowerResponse.kt: -------------------------------------------------------------------------------- 1 | package com.example.model.github 2 | 3 | @kotlinx.serialization.Serializable 4 | data class GithubFollowerResponseItem( 5 | val gistsUrl: String? = null, 6 | val reposUrl: String? = null, 7 | val followingUrl: String? = null, 8 | val starredUrl: String? = null, 9 | val login: String? = null, 10 | val followersUrl: String? = null, 11 | val type: String? = null, 12 | val url: String? = null, 13 | val subscriptionsUrl: String? = null, 14 | val receivedEventsUrl: String? = null, 15 | val avatarUrl: String? = null, 16 | val eventsUrl: String? = null, 17 | val htmlUrl: String? = null, 18 | val siteAdmin: Boolean? = null, 19 | val id: Int? = null, 20 | val gravatarId: String? = null, 21 | val nodeId: String? = null, 22 | val organizationsUrl: String? = null 23 | ) 24 | -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi2.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.api.StarWarsApi 4 | import com.example.model.People 5 | import de.jensklingenberg.ktorfit.http.GET 6 | import de.jensklingenberg.ktorfit.http.Path 7 | import de.jensklingenberg.ktorfit.http.QueryName 8 | 9 | interface TestApi2 : 10 | StarWarsApi, 11 | QueryNameTestApi { 12 | @GET("people/{id}/") 13 | fun tste() 14 | } 15 | 16 | data class Test( 17 | val name: String 18 | ) 19 | 20 | interface QueryNameTestApi { 21 | @GET("people/{id}/") 22 | suspend fun testQueryName( 23 | @Path("id") peopleId: Int, 24 | @QueryName name: String 25 | ): People 26 | 27 | @GET("people/{id}/") 28 | suspend fun testQueryNameList( 29 | @Path("id") peopleId: Int, 30 | @QueryName(false) name: List 31 | ): People 32 | } 33 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/KtorfitLogger.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.google.devtools.ksp.processing.KSPLogger 4 | import com.google.devtools.ksp.symbol.KSNode 5 | 6 | class KtorfitLogger( 7 | private val kspLogger: KSPLogger, 8 | private val loggingType: Int 9 | ) : KSPLogger by kspLogger { 10 | override fun error( 11 | message: String, 12 | symbol: KSNode?, 13 | ) { 14 | when (loggingType) { 15 | 0 -> { 16 | // Do nothing 17 | } 18 | 19 | 1 -> { 20 | // Throw compile errors for Ktorfit 21 | kspLogger.error("Ktorfit: $message", symbol) 22 | } 23 | 24 | 2 -> { 25 | // Turn errors into compile warnings 26 | kspLogger.warn("Ktorfit: $message", symbol) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | compileSdk = 36 8 | defaultConfig { 9 | applicationId = "com.example.myapplication.android" 10 | minSdk = 21 11 | targetSdk = 34 12 | versionCode = 1 13 | versionName = "1.0" 14 | namespace = "com.example.myapplication.android" 15 | } 16 | buildTypes { 17 | getByName("release") { 18 | isMinifyEnabled = false 19 | } 20 | } 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | kotlinOptions { 28 | jvmTarget= "1.8" 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation(project(":shared")) 34 | implementation("com.google.android.material:material:1.13.0") 35 | } -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/StarWarsApi.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample 2 | 3 | import de.jensklingenberg.ktorfit.Call 4 | import de.jensklingenberg.ktorfit.Response 5 | import de.jensklingenberg.ktorfit.http.GET 6 | import de.jensklingenberg.ktorfit.http.Path 7 | import de.jensklingenberg.ktorfit.http.Query 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | interface StarWarsApi { 11 | 12 | companion object { 13 | const val baseUrl = "https://swapi.info/api/" 14 | } 15 | 16 | @GET("people/{id}/") 17 | suspend fun getPerson(@Path("id") personId: Int): Person 18 | 19 | @GET("people") 20 | fun getPeopleFlow(@Query("page") page: Int): Flow 21 | 22 | @GET("people/{id}/") 23 | fun getPersonCall(@Path("id") personId: Int): Call 24 | 25 | @GET("people/{id}/") 26 | suspend fun getPersonResponse(@Path("id") personId: Int): Response 27 | 28 | } -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Path.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * This can be set if you have parts in your URL that want to dynamically replaced 5 | * 6 | * @param value The default value will be replaced with the name of the parameter that is annotated. 7 | * When the URL of an HTTP Method Annotation contains curly braces, they will be replaced with the value of 8 | * the corresponding parameter that has a matching [value]. 9 | * @param encoded true means that this value is already URL encoded and will not be encoded again 10 | * Path parameters type may not be nullable. 11 | * 12 | * 13 | * ``` 14 | * @GET("post/{postId}") 15 | * suspend fun getPosts(@Path("postId") postId: Int): List 16 | * ``` 17 | */ 18 | 19 | @Target(AnnotationTarget.VALUE_PARAMETER) 20 | annotation class Path( 21 | val value: String = "KTORFIT_DEFAULT_VALUE", 22 | val encoded: Boolean = false 23 | ) 24 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | # Update Ktorfit for new Kotlin version 4 | - Bump **kotlin** in libs.versions 5 | - Change **ktorfitCompiler** in libs.versions to KTORFIT_VERSION-NEW_KOTLIN_VERSION 6 | - Run tests in :ktorfit-compiler-plugin 7 | - Create a PR against master 8 | - Merge PR 9 | - Run GitHub Actions "publish" workflow 10 | 11 | ## 👷 Project Structure 12 | 13 | * compiler plugin - module with source for the compiler plugin 14 | * ktorfit-annotations - module with annotations for the Ktorfit 15 | * ktorfit-ksp - module with source for the KSP plugin 16 | * ktorfit-lib-core - module with source for the Ktorfit lib 17 | * ktorfit-lib - ktorfit-lib-core + dependencies on platform specific clients 18 | * sandbox - experimental test module to try various stuff 19 | 20 | * example - contains example projects that use Ktorfit 21 | * docs - contains the source for the GitHub page 22 | -------------------------------------------------------------------------------- /docs/converters/requestparameterconverter.md: -------------------------------------------------------------------------------- 1 | # RequestParameterConverter 2 | 3 | ```kotlin 4 | @GET("posts/{postId}/comments") 5 | suspend fun getCommentsById(@RequestType(Int::class) @Path("postId") postId: String): List 6 | ``` 7 | 8 | You can set RequestType at a parameter with a type to which the parameter should be converted. 9 | 10 | Then you need to implement a Converter factory with a RequestParameterConverter. 11 | 12 | ```kotlin 13 | class StringToIntRequestConverterFactory : Converter.Factory { 14 | override fun requestParameterConverter( 15 | parameterType: KClass<*>, 16 | requestType: KClass<*> 17 | ): Converter.RequestParameterConverter? { 18 | return object : Converter.RequestParameterConverter { 19 | override fun convert(data: Any): Any { 20 | //convert the data 21 | } 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | ```kotlin 28 | ktorfit.converterFactories(StringToIntRequestConverterFactory()) 29 | ``` -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #versions 2 | 3 | org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=8g 4 | 5 | android.defaults.buildfeatures.buildconfig = false 6 | android.useAndroidX=true 7 | 8 | # kotlin 9 | kotlin.incremental=true 10 | kotlin.compiler.execution.strategy=in-process 11 | kotlin.native.ignoreDisabledTargets=true 12 | kotlin.native.binary.freezing=disabled 13 | 14 | # Maven Central 15 | POM_NAME=Ktorfit 16 | POM_DESCRIPTION=Ktorfit 17 | POM_INCEPTION_YEAR=2022 18 | POM_URL=https://github.com/Foso/Ktorfit 19 | POM_SCM_URL=https://github.com/Foso/Ktorfit 20 | POM_SCM_CONNECTION=scm:https://github.com/Foso/Ktorfit.git 21 | POM_SCM_DEV_CONNECTION=scm:git://github.com/Foso/Ktorfit.git 22 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 23 | POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt 24 | POM_LICENCE_DIST=repo 25 | POM_DEVELOPER_ID=Foso 26 | POM_DEVELOPER_NAME=Jens Klingenberg 27 | POM_DEVELOPER_URL=https://www.jensklingenberg.de 28 | SONATYPE_STAGING_PROFILE=de.jensklingenberg 29 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/shared/shared.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/shared 3 | ENABLE_USER_SCRIPT_SANDBOXING = NO 4 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | KOTLIN_PROJECT_PATH = :shared 7 | OTHER_LDFLAGS = $(inherited) -l"c++" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 11 | PODS_ROOT = ${SRCROOT} 12 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../shared 13 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 14 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 15 | PRODUCT_MODULE_NAME = shared 16 | SKIP_INSTALL = YES 17 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 18 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/shared/shared.release.xcconfig: -------------------------------------------------------------------------------- 1 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO 2 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/shared 3 | ENABLE_USER_SCRIPT_SANDBOXING = NO 4 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/../../shared/build/cocoapods/framework" 5 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 6 | KOTLIN_PROJECT_PATH = :shared 7 | OTHER_LDFLAGS = $(inherited) -l"c++" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} 11 | PODS_ROOT = ${SRCROOT} 12 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../shared 13 | PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates 14 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 15 | PRODUCT_MODULE_NAME = shared 16 | SKIP_INSTALL = YES 17 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 18 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | # Publish new version 5 | 6 | 1. Create new branch `release/X.Y.Z` from `master` branch 7 | 2. Update **ktorfit** version inside `gradle/libs.versions.toml` 8 | 3. Update **ktorfitGradlePlugin** version inside `gradle/libs.versions.toml` 9 | 4. Update Compatibility table in Readme.md 10 | 5. Update KtorfitCompilerSubPlugin.defaultCompilerPluginVersion if necessary 11 | 6. Update ktorfit release version in mkdocs.yml 12 | 7. Update version in KtorfitGradleConfiguration 13 | 8. Set the release date in docs/changelog.md 14 | 9. `git commit -am "X.Y.Z."` (where X.Y.Z is the new version) 15 | 10. Push and create a PR to the `master` branch 16 | 11. When all checks successful, run GitHub Action `Publish Release` from your branch 17 | 12. Set the Git tag `git tag -a X.Y.Z -m "X.Y.Z"` (where X.Y.Z is the new version) 18 | 13. Merge the PR 19 | 14. Create a new release with for the Tag on GitHub 20 | 15. Run "deploy to GitHub pages" action 21 | 16. Put the relevant changelog in the release description 22 | -------------------------------------------------------------------------------- /ktorfit-ksp/detekt-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ComplexMethod:FunctionsParser.kt$fun getFunctionDataList( ksFunctionDeclarationList: List<KSFunctionDeclaration>, logger: KSPLogger ): List<FunctionData> 6 | ComplexMethod:ParameterParser.kt$fun getParamAnnotationList(ksValueParameter: KSValueParameter, logger: KSPLogger): List<ParameterAnnotation> 7 | LongMethod:FunctionsParser.kt$fun getFunctionDataList( ksFunctionDeclarationList: List<KSFunctionDeclaration>, logger: KSPLogger ): List<FunctionData> 8 | LongMethod:ParameterParser.kt$fun getParamAnnotationList(ksValueParameter: KSValueParameter, logger: KSPLogger): List<ParameterAnnotation> 9 | TooManyFunctions:KSValueParameterExt.kt$de.jensklingenberg.ktorfit.utils.KSValueParameterExt.kt 10 | TooManyFunctions:Utils.kt$de.jensklingenberg.ktorfit.utils.Utils.kt 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Target Support Files/Pods-iosApp/Pods-iosApp-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | ${PODS_DEVELOPMENT_LANGUAGE} 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/Utils.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.tschuchort.compiletesting.KotlinCompilation 4 | import com.tschuchort.compiletesting.SourceFile 5 | import com.tschuchort.compiletesting.configureKsp 6 | import com.tschuchort.compiletesting.kspIncremental 7 | import com.tschuchort.compiletesting.kspProcessorOptions 8 | import com.tschuchort.compiletesting.kspWithCompilation 9 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 10 | 11 | @OptIn(ExperimentalCompilerApi::class) 12 | fun getCompilation( 13 | sources: List, 14 | kspArgs: MutableMap = mutableMapOf(), 15 | ): KotlinCompilation = 16 | KotlinCompilation().apply { 17 | this.sources = sources 18 | inheritClassPath = true 19 | 20 | configureKsp { 21 | kspProcessorOptions = kspArgs 22 | symbolProcessorProviders += KtorfitProcessorProvider() 23 | } 24 | kspIncremental = true 25 | kspWithCompilation = true 26 | } 27 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonTest/kotlin/de/jensklingenberg/ktorfit/TestStringToIntRequestConverter.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import de.jensklingenberg.ktorfit.converter.Converter 4 | import kotlin.reflect.KClass 5 | 6 | class TestStringToIntRequestConverter : Converter.Factory { 7 | private fun supportedType( 8 | parameterType: KClass<*>, 9 | requestType: KClass<*>, 10 | ): Boolean { 11 | val parameterIsString = parameterType == String::class 12 | val requestIsInt = requestType == Int::class 13 | return parameterIsString && requestIsInt 14 | } 15 | 16 | class Test : Converter.RequestParameterConverter { 17 | override fun convert(data: Any): Any { 18 | return (data as String).toInt() 19 | } 20 | } 21 | 22 | override fun requestParameterConverter( 23 | parameterType: KClass<*>, 24 | requestType: KClass<*>, 25 | ): Converter.RequestParameterConverter? { 26 | if (!supportedType(parameterType, requestType)) return null 27 | return Test() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ktorfit-annotations/src/commonMain/kotlin/de/jensklingenberg/ktorfit/http/Query.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.http 2 | 3 | /** 4 | * Used for query parameters 5 | * 6 | * ``` 7 | * @GET("comments") 8 | * suspend fun getCommentsById(@Query("postId") postId: String): List 9 | * ``` 10 | * A request with getCommentsById(3) will result in the relative URL “comments?postId=3” 11 | * 12 | * ``` 13 | * @GET("comments") 14 | * suspend fun getCommentsById(@Query("postId") postId: List): List 15 | * ``` 16 | * 17 | * A request with getCommentsById(listOf("3",null,"4")) will result in the relative URL “comments?postId=3&postId=4” 18 | * 19 | * @param value The default value will be replaced with the name of the parameter that is annotated.It is the key of the query parameter. 20 | * null values are ignored 21 | * @param encoded true means that this value is already URL encoded and will not be encoded again 22 | */ 23 | @Target(AnnotationTarget.VALUE_PARAMETER) 24 | annotation class Query( 25 | val value: String = "KTORFIT_DEFAULT_VALUE", 26 | val encoded: Boolean = false 27 | ) 28 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonTest/kotlin/de/jensklingenberg/ktorfit/internal/TypeDataTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.internal 2 | 3 | import de.jensklingenberg.ktorfit.converter.TypeData 4 | import io.ktor.util.reflect.typeInfo 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertTrue 8 | 9 | class TypeDataTest { 10 | @Test 11 | fun testTypeDataCreator() { 12 | val typeData = TypeData.createTypeData("kotlin.Map", typeInfo>()) 13 | 14 | assertEquals("kotlin.Map", typeData.qualifiedName) 15 | assertTrue(typeData.typeInfo.type == Map::class) 16 | assertTrue(typeData.typeArgs[0].typeInfo.type == String::class) 17 | } 18 | 19 | @Test 20 | fun testTypeDataCreatorWithEmptyQualifiedName() { 21 | val typeData = TypeData.createTypeData("", typeInfo>()) 22 | 23 | assertEquals("", typeData.qualifiedName) 24 | assertTrue(typeData.typeInfo.type == Map::class) 25 | assertTrue(typeData.typeArgs[0].typeInfo.type == String::class) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/publish-converters.yml: -------------------------------------------------------------------------------- 1 | name: Publish Converters 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-converters: 8 | runs-on: macos-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v6 12 | 13 | - name: Install JDK 21 14 | uses: actions/setup-java@v5 15 | with: 16 | distribution: 'temurin' 17 | java-version: 21 18 | 19 | - uses: gradle/actions/setup-gradle@v5 20 | 21 | - name: Publish release 22 | run: ./gradlew :ktorfit-converters:call:publishAllPublicationsToMavenCentralRepository :ktorfit-converters:flow:publishAllPublicationsToMavenCentralRepository :ktorfit-converters:response:publishAllPublicationsToMavenCentralRepository 23 | env: 24 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 25 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} 28 | -------------------------------------------------------------------------------- /ktorfit-converters/call/api/jvm/call.api: -------------------------------------------------------------------------------- 1 | public abstract interface class de/jensklingenberg/ktorfit/Call { 2 | public abstract fun onExecute (Lde/jensklingenberg/ktorfit/Callback;)V 3 | } 4 | 5 | public abstract interface class de/jensklingenberg/ktorfit/Callback { 6 | public abstract fun onError (Ljava/lang/Throwable;)V 7 | public abstract fun onResponse (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)V 8 | } 9 | 10 | public final class de/jensklingenberg/ktorfit/converter/CallConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 11 | public fun ()V 12 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 13 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 14 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ktorfit-converters/call/api/android/call.api: -------------------------------------------------------------------------------- 1 | public abstract interface class de/jensklingenberg/ktorfit/Call { 2 | public abstract fun onExecute (Lde/jensklingenberg/ktorfit/Callback;)V 3 | } 4 | 5 | public abstract interface class de/jensklingenberg/ktorfit/Callback { 6 | public abstract fun onError (Ljava/lang/Throwable;)V 7 | public abstract fun onResponse (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)V 8 | } 9 | 10 | public final class de/jensklingenberg/ktorfit/converter/CallConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 11 | public fun ()V 12 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 13 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 14 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/CommonClient.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | import com.example.api.JsonPlaceHolderApi 4 | import com.example.api.createJsonPlaceHolderApi 5 | import de.jensklingenberg.ktorfit.converter.CallConverterFactory 6 | import de.jensklingenberg.ktorfit.ktorfit 7 | import io.ktor.client.* 8 | import io.ktor.client.plugins.contentnegotiation.* 9 | import io.ktor.serialization.kotlinx.json.* 10 | import kotlinx.serialization.json.Json 11 | 12 | val commonClient = 13 | HttpClient { 14 | 15 | install(ContentNegotiation) { 16 | json( 17 | Json { 18 | isLenient = true 19 | ignoreUnknownKeys = true 20 | } 21 | ) 22 | } 23 | } 24 | 25 | val commonKtorfit = 26 | ktorfit { 27 | baseUrl(JsonPlaceHolderApi.baseUrl) 28 | httpClient(commonClient) 29 | converterFactories( 30 | CallConverterFactory(), 31 | StringToIntRequestConverterFactory(), 32 | MyOwnResponseConverterFactory() 33 | ) 34 | } 35 | 36 | val jsonPlaceHolderApi = commonKtorfit.createJsonPlaceHolderApi() 37 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/model/KtorfitClass.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.model 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | 5 | data class KtorfitClass( 6 | val name: String, 7 | val packageName: String, 8 | val objectName: String 9 | ) 10 | 11 | val ktorfitClass = KtorfitClass("Ktorfit", "de.jensklingenberg.ktorfit", "_ktorfit") 12 | val providerClass = KtorfitClass("ClassProvider", "de.jensklingenberg.ktorfit.internal", "EMPTY") 13 | val extDataClass = KtorfitClass("HttpRequestBuilder.() -> Unit", "", "_ext") 14 | val formParameters = KtorfitClass("", "", "__formParameters") 15 | val formData = KtorfitClass("", "", "__formData") 16 | val converterHelper = KtorfitClass("KtorfitConverterHelper", "de.jensklingenberg.ktorfit.internal", "_helper") 17 | val httpClientClass = KtorfitClass("HttpClient", "io.ktor.client", "_httpClient") 18 | val internalApi = ClassName("de.jensklingenberg.ktorfit.internal", "InternalKtorfitApi") 19 | val annotationsAttributeKey = KtorfitClass("annotationsAttributeKey", "de.jensklingenberg.ktorfit", "AttributeKey(\"__ktorfit_attribute_annotations\")") 20 | 21 | fun KtorfitClass.toClassName() = ClassName(packageName, name) 22 | -------------------------------------------------------------------------------- /example/MultiplatformExample/androidApp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/model/StringToIntRequestConverter.kt: -------------------------------------------------------------------------------- 1 | package com.example.model 2 | 3 | import de.jensklingenberg.ktorfit.converter.Converter 4 | import kotlin.reflect.KClass 5 | 6 | class StringToIntRequestConverterFactory : Converter.Factory { 7 | class StringToIntRequestConverter : Converter.RequestParameterConverter { 8 | override fun convert(data: Any): Any = (data as String).toInt() 9 | } 10 | 11 | private fun supportedType( 12 | parameterType: KClass<*>, 13 | requestType: KClass<*> 14 | ): Boolean { 15 | val parameterIsString = parameterType == String::class 16 | val requestIsInt = requestType == Int::class 17 | return parameterIsString && requestIsInt 18 | } 19 | 20 | override fun requestParameterConverter( 21 | parameterType: KClass<*>, 22 | requestType: KClass<*> 23 | ): Converter.RequestParameterConverter? { 24 | if (supportedType(parameterType, requestType)) { 25 | return object : Converter.RequestParameterConverter { 26 | override fun convert(data: Any): Any = (data as String).toInt() 27 | } 28 | } 29 | return null 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/GetBodyDataTextKtTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import com.google.devtools.ksp.symbol.KSType 4 | import de.jensklingenberg.ktorfit.model.ParameterData 5 | import de.jensklingenberg.ktorfit.model.ReturnTypeData 6 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Body 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Test 9 | import org.mockito.kotlin.mock 10 | 11 | class GetBodyDataTextKtTest { 12 | @Test 13 | fun testWithoutBodyAnnotation() { 14 | val parameterData = ParameterData("test1", ReturnTypeData("String", mock())) 15 | val params = listOf(parameterData) 16 | val text = getBodyDataText(params) 17 | assertEquals("", text) 18 | } 19 | 20 | @Test 21 | fun testWithBodyAnnotation() { 22 | val bodyAnno = Body 23 | val parameterData = 24 | ParameterData("test1", ReturnTypeData("Map<*,String>", mock()), annotations = listOf(bodyAnno)) 25 | val params = listOf(parameterData) 26 | val text = getBodyDataText(params) 27 | assertEquals("setBody(test1)", text) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/TestApi.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.model.Post 4 | import de.jensklingenberg.ktorfit.Call 5 | import de.jensklingenberg.ktorfit.http.Body 6 | import de.jensklingenberg.ktorfit.http.GET 7 | import de.jensklingenberg.ktorfit.http.Headers 8 | import de.jensklingenberg.ktorfit.http.POST 9 | import de.jensklingenberg.ktorfit.http.Path 10 | import io.ktor.client.request.forms.MultiPartFormDataContent 11 | import kotlinx.coroutines.flow.Flow 12 | 13 | interface TestApi { 14 | @GET("pos4ts") 15 | fun getPosts(): Call> 16 | 17 | @GET("posts/{userId}") 18 | suspend fun getPost( 19 | @Path("userId") myUserId: Int = 4 20 | ): Post 21 | 22 | @POST("posts") 23 | suspend fun postPost( 24 | @Body otherID: Post 25 | ): Post 26 | 27 | @GET("posts/{userId}") 28 | suspend fun getPostsByUserId( 29 | @Path("userId") myUserId: Int 30 | ): List 31 | 32 | @Headers(value = ["Accept: application/json"]) 33 | @GET("posts") 34 | fun getFlowPosts(): Flow> 35 | 36 | @POST("upload") 37 | suspend fun uppi( 38 | @Body map: MultiPartFormDataContent 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug","unconfirmed"] 4 | body: 5 | - type: input 6 | id: ktorfit-version 7 | attributes: 8 | label: Ktorfit version 9 | placeholder: ex. 2.x.x 10 | validations: 11 | required: true 12 | - type: markdown 13 | attributes: 14 | value: | 15 | Thanks for taking the time to fill out this bug report! 16 | - type: textarea 17 | id: what-happened 18 | attributes: 19 | label: What happened and how can we reproduce this issue? 20 | description: as minimally and precisely as possible please 21 | placeholder: Give us a nice bullited list of things that lead up to this bug 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: expexted 26 | attributes: 27 | label: What did you expect to happen? 28 | description: What would happen if everything was going smoothly? 29 | validations: 30 | required: true 31 | - type: textarea 32 | id: extra-details 33 | attributes: 34 | label: Is there anything else we need to know about? 35 | description: Tell us the rest of the story 36 | validations: 37 | required: false 38 | 39 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | labels: ["enhancement"] 4 | body: 5 | - type: textarea 6 | id: related-problem 7 | attributes: 8 | label: Is your feature request related to a problem? Please describe. 9 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | validations: 11 | required: false 12 | - type: textarea 13 | id: describe-solution 14 | attributes: 15 | label: Describe the solution you'd like 16 | description: A clear and concise description of what you want to happen. 17 | placeholder: 18 | validations: 19 | required: false 20 | - type: textarea 21 | id: considered 22 | attributes: 23 | label: Describe alternatives you've considered 24 | description: A clear and concise description of any alternative solutions or features you've considered. 25 | validations: 26 | required: false 27 | - type: textarea 28 | id: additional-context 29 | attributes: 30 | label: Additional context 31 | description: Add any other context or screenshots about the feature request here. 32 | validations: 33 | required: false 34 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonTest/kotlin/de/jensklingenberg/ktorfit/converter/RequestParameterConverterTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import de.jensklingenberg.ktorfit.TestEngine 5 | import de.jensklingenberg.ktorfit.TestStringToIntRequestConverter 6 | import de.jensklingenberg.ktorfit.internal.InternalKtorfitApi 7 | import de.jensklingenberg.ktorfit.internal.KtorfitConverterHelper 8 | import io.ktor.client.request.HttpRequestData 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | class RequestParameterConverterTest { 13 | @OptIn(InternalKtorfitApi::class) 14 | @Test 15 | fun testRequestConverter() { 16 | val engine = 17 | object : TestEngine() { 18 | override fun getRequestData(data: HttpRequestData) { 19 | } 20 | } 21 | 22 | val ktorfit = 23 | Ktorfit.Builder().httpClient( 24 | engine, 25 | ).baseUrl("http://www.test.de/").converterFactories(TestStringToIntRequestConverter()).build() 26 | 27 | val converted = KtorfitConverterHelper(ktorfit).convertParameterType("4", String::class, Int::class) 28 | assertEquals(4, converted) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ktorfit-converters/flow/api/flow.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final class de.jensklingenberg.ktorfit.converter/FlowConverterFactory : de.jensklingenberg.ktorfit.converter/Converter.Factory { // de.jensklingenberg.ktorfit.converter/FlowConverterFactory|null[0] 10 | constructor () // de.jensklingenberg.ktorfit.converter/FlowConverterFactory.|(){}[0] 11 | 12 | final fun responseConverter(de.jensklingenberg.ktorfit.converter/TypeData, de.jensklingenberg.ktorfit/Ktorfit): de.jensklingenberg.ktorfit.converter/Converter.ResponseConverter? // de.jensklingenberg.ktorfit.converter/FlowConverterFactory.responseConverter|responseConverter(de.jensklingenberg.ktorfit.converter.TypeData;de.jensklingenberg.ktorfit.Ktorfit){}[0] 13 | } 14 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/commonMain/kotlin/com/example/ktorfittest/StarWarsApi.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import de.jensklingenberg.ktorfit.Call 4 | import de.jensklingenberg.ktorfit.Response 5 | import de.jensklingenberg.ktorfit.http.GET 6 | import de.jensklingenberg.ktorfit.http.Path 7 | import de.jensklingenberg.ktorfit.http.Query 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | interface StarWarsApi { 11 | companion object { 12 | const val baseUrl = "https://swapi.info/api/" 13 | } 14 | 15 | @GET("people/{id}/") 16 | suspend fun getPersonByIdResponse( 17 | @Path("id") peopleId: Int 18 | ): Person 19 | 20 | @GET("people/{id}/") 21 | fun getPeopleByIdFlowResponse( 22 | @Path("id") peopleId: Int, 23 | @Query("hello") world: String? 24 | ): Flow 25 | 26 | @GET("people/{id}/") 27 | fun getPeopleByIdCallResponse( 28 | @Path("id") peopleId: Int 29 | ): Call 30 | 31 | @GET("people/{id}/") 32 | fun queryTest( 33 | @Path("id") peopleId: Int, 34 | @Query("hello") world: String? 35 | ): Call 36 | 37 | @GET("people/{id}/") 38 | suspend fun getPersonResponse( 39 | @Path("id") personId: Int 40 | ): Response 41 | } 42 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/GetRequestBuilderTextKtTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import com.google.devtools.ksp.symbol.KSType 4 | import de.jensklingenberg.ktorfit.model.ParameterData 5 | import de.jensklingenberg.ktorfit.model.ReturnTypeData 6 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.RequestBuilder 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Test 9 | import org.mockito.kotlin.mock 10 | 11 | class GetRequestBuilderTextKtTest { 12 | @Test 13 | fun testWithoutRequestBuilderAnnotation() { 14 | val parameterData = ParameterData("test1", ReturnTypeData("String", mock())) 15 | val params = listOf(parameterData) 16 | val text = getCustomRequestBuilderText(params) 17 | assertEquals("", text) 18 | } 19 | 20 | @Test 21 | fun testWithRequestBuilderAnnotation() { 22 | val bodyAnno = RequestBuilder 23 | val parameterData = 24 | ParameterData("test1", ReturnTypeData("String", mock()), annotations = listOf(bodyAnno)) 25 | val params = listOf(parameterData) 26 | val text = getCustomRequestBuilderText(params) 27 | assertEquals("test1(this)", text) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub pages 2 | on: 3 | # schedule: 4 | # - cron: '30 5,17 * * *' 5 | workflow_dispatch: 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v6 11 | with: 12 | submodules: "recursive" 13 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 14 | - name: Setup Python 15 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 16 | with: 17 | python-version: '3.14' 18 | architecture: 'x64' 19 | - name: Install dependencies 20 | run: | 21 | python3 -m pip install --upgrade pip # install pip 22 | python3 -m pip install mkdocs # install mkdocs 23 | python3 -m pip install mkdocs-material # install material theme 24 | python3 -m pip install mkdocs-git-revision-date-localized-plugin 25 | python3 -m pip install mkdocs-minify-plugin 26 | python3 -m pip install mkdocs-macros-plugin 27 | - name: Build site 28 | run: mkdocs build 29 | - name: Deploy 30 | uses: peaceiris/actions-gh-pages@v4 31 | with: 32 | personal_token: ${{ secrets.GH_SECRET }} 33 | publish_dir: ./site 34 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/builtin/DontSwallowExceptionsConverterFactory.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter.builtin 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import de.jensklingenberg.ktorfit.converter.Converter 5 | import de.jensklingenberg.ktorfit.converter.KtorfitResult 6 | import de.jensklingenberg.ktorfit.converter.TypeData 7 | import io.ktor.client.statement.HttpResponse 8 | 9 | public class DontSwallowExceptionsConverterFactory : Converter.Factory { 10 | private class DefaultSuspendResponseConverter( 11 | val typeData: TypeData 12 | ) : Converter.SuspendResponseConverter { 13 | override suspend fun convert(result: KtorfitResult): Any? = 14 | when (result) { 15 | is KtorfitResult.Failure -> { 16 | throw result.throwable 17 | } 18 | 19 | is KtorfitResult.Success -> { 20 | result.response.call.body(typeData.typeInfo) 21 | } 22 | } 23 | } 24 | 25 | override fun suspendResponseConverter( 26 | typeData: TypeData, 27 | ktorfit: Ktorfit, 28 | ): Converter.SuspendResponseConverter = DefaultSuspendResponseConverter(typeData) 29 | } 30 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp.xcodeproj/xcuserdata/jens.klingenberg.xcuserdatad/xcschemes/iosApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/src/main/java/de/jensklingenberg/ktorfit/CommonCompilerPluginRegistrar.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.google.auto.service.AutoService 4 | import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 9 | import org.jetbrains.kotlin.config.CompilerConfiguration 10 | 11 | @OptIn(ExperimentalCompilerApi::class) 12 | @AutoService(CompilerPluginRegistrar::class) 13 | class CommonCompilerPluginRegistrar : CompilerPluginRegistrar() { 14 | override val supportsK2: Boolean 15 | get() = true 16 | 17 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 18 | if (configuration[KEY_ENABLED] == false) { 19 | return 20 | } 21 | val logging = configuration[KEY_LOGGING] ?: false 22 | val messageCollector = configuration.get(CommonConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE) 23 | 24 | IrGenerationExtension.registerExtension( 25 | KtorfitIrGenerationExtension(DebugLogger(logging, messageCollector)), 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/assets/badges/platforms.svg: -------------------------------------------------------------------------------- 1 | Platform: Android | JVM | iOS | macOS | watchOS | tvOS | JS | Linux | WindowsPlatformAndroid | JVM | iOS | macOS | watchOS | tvOS | JS | Linux | Windows -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/HeaderTestApi.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.model.People 4 | import de.jensklingenberg.ktorfit.http.GET 5 | import de.jensklingenberg.ktorfit.http.Header 6 | import de.jensklingenberg.ktorfit.http.HeaderMap 7 | import de.jensklingenberg.ktorfit.http.Headers 8 | import de.jensklingenberg.ktorfit.http.Path 9 | 10 | interface HeaderTestApi { 11 | @GET("people/{id}/") 12 | suspend fun multipleHeader( 13 | @Path("id") peopleId: Int, 14 | @Header("huhu") name: Array, 15 | @Header("hey") name2: String 16 | ): People 17 | 18 | @GET("people/{id}/") 19 | suspend fun testHeaderWithArray( 20 | @Path("id") peopleId: Int, 21 | @Header("huhu") name: Array 22 | ): People 23 | 24 | @GET("people/{id}/") 25 | suspend fun testHeaderWithList( 26 | @Path("id") peopleId: Int, 27 | @Header("huhu") name: List 28 | ): People 29 | 30 | @Headers("Accept2: application/json", "Accept: application/json2") 31 | @GET("people/{id}/") 32 | suspend fun testHeaders( 33 | @Path("id") peopleId: Int 34 | ): People 35 | 36 | @GET("people/{id}/") 37 | suspend fun testHeaderMap( 38 | @Path("id") peopleId: Int, 39 | @HeaderMap() name: Map? 40 | ): People 41 | } 42 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/java/de/jensklingenberg/androidonlyexample/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.androidonlyexample.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun AndroidOnlyExampleTheme( 32 | darkTheme: Boolean = isSystemInDarkTheme(), 33 | content: @Composable () -> Unit 34 | ) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /docs/converters/converters.md: -------------------------------------------------------------------------------- 1 | Converters are used to convert the HTTPResponse or parameters. 2 | 3 | They are added inside of a Converter.Factory which will then be added to the Ktorfit builder with the **converterfactories()** function. 4 | 5 | ### Converter Types 6 | * [ResponseConverters](./responseconverter.md) 7 | * [SuspendResponseConverter](./suspendresponseconverter.md) 8 | * [RequestParameterConverter](./requestparameterconverter.md) 9 | 10 | ### Existing converter factories 11 | * CallConverterFactory 12 | 13 | Add this dependency: 14 | ```kotlin 15 | implementation("de.jensklingenberg.ktorfit:ktorfit-converters-call:$CONVERTER_VERSION") 16 | ``` 17 | 18 | You can find all available versions [here](https://repo.maven.apache.org/maven2/de/jensklingenberg/ktorfit/ktorfit-converters-call/) 19 | 20 | * FlowConverterFactory 21 | 22 | Add this dependency: 23 | ```kotlin 24 | implementation("de.jensklingenberg.ktorfit:ktorfit-converters-flow:$CONVERTER_VERSION") 25 | ``` 26 | 27 | You can find all available versions [here](https://repo.maven.apache.org/maven2/de/jensklingenberg/ktorfit/ktorfit-converters-flow/) 28 | 29 | * ResponseConverterFactory 30 | 31 | Add this dependency: 32 | ```kotlin 33 | implementation("de.jensklingenberg.ktorfit:ktorfit-converters-response:$CONVERTER_VERSION") 34 | ``` 35 | 36 | You can find all available versions [here](https://repo.maven.apache.org/maven2/de/jensklingenberg/ktorfit/ktorfit-converters-response/) 37 | 38 | -------------------------------------------------------------------------------- /docs/theme/404.html: -------------------------------------------------------------------------------- 1 | 22 | 23 | {% extends "base.html" %} 24 | 25 | 26 | {% block content %} 27 |

404 - Not found

28 | 29 | Can't find this file go to: Overview 30 | 31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /docs/theme/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% if page.edit_url %} 5 | {% if "/api/" in page.edit_url %} 6 | 🤔 Documentation issue? Report it 7 | {% else %} 8 | 🤔 Documentation issue? Report or edit 9 | {% endif %} 10 | {% endif %} 11 | {% if not "\x3ch1" in page.content %} 12 |

{{ page.title | default(config.site_name, true)}}

13 | {% endif %} 14 | {{ page.content }} 15 |
16 | {% if page.meta.git_revision_date_localized %} 17 | Last update: {{ page.meta.git_revision_date_localized }} 18 | {% endif %} 19 | {% block source %} 20 | {% if page and page.meta and page.meta.source %} 21 |

{{ lang.t("meta.source") }}

22 | {% set repo = config.repo_url %} 23 | {% if repo | last == "/" %} 24 | {% set repo = repo[:-1] %} 25 | {% endif %} 26 | {% set path = page.meta.path | default([""]) %} 27 | {% set file = page.meta.source %} 28 | 29 | {{ file }} 30 | 31 | {% endif %} 32 | {% endblock %} 33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/RequestTypeTest.kt: -------------------------------------------------------------------------------- 1 | import com.tschuchort.compiletesting.SourceFile 2 | import com.tschuchort.compiletesting.kspSourcesDir 3 | import de.jensklingenberg.ktorfit.getCompilation 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | import java.io.File 7 | 8 | class RequestTypeTest { 9 | @Test 10 | fun generate() { 11 | val source = 12 | SourceFile.kotlin( 13 | "Source.kt", 14 | """ 15 | package com.example.api 16 | import de.jensklingenberg.ktorfit.http.* 17 | 18 | interface TestService { 19 | @GET("posts/{postId}/comments") 20 | suspend fun test(@RequestType(Int::class) @Path("postId") postId: String, @Query("postId") testQuery: String): String 21 | } 22 | """, 23 | ) 24 | 25 | val expectedFunctionSource = 26 | """val postId: Int = _helper.convertParameterType(postId,postId::class,Int::class)""" 27 | 28 | val compilation = getCompilation(listOf(source)) 29 | val result = compilation.compile() 30 | 31 | val generatedSourcesDir = compilation.kspSourcesDir 32 | val generatedFile = 33 | File( 34 | generatedSourcesDir, 35 | "/kotlin/com/example/api/_TestServiceImpl.kt", 36 | ) 37 | val actualSource = generatedFile.readText() 38 | assertTrue(actualSource.contains(expectedFunctionSource)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/UserFactory.kt: -------------------------------------------------------------------------------- 1 | package com.example 2 | 3 | import com.example.model.Envelope 4 | import com.example.model.User 5 | import de.jensklingenberg.ktorfit.Ktorfit 6 | import de.jensklingenberg.ktorfit.converter.Converter 7 | import de.jensklingenberg.ktorfit.converter.KtorfitResult 8 | import de.jensklingenberg.ktorfit.converter.TypeData 9 | import io.ktor.client.call.* 10 | import io.ktor.client.statement.* 11 | 12 | class UserFactory : Converter.Factory { 13 | override fun suspendResponseConverter( 14 | typeData: TypeData, 15 | ktorfit: Ktorfit 16 | ): Converter.SuspendResponseConverter? { 17 | if (typeData.typeInfo.type == User::class) { 18 | return object : Converter.SuspendResponseConverter { 19 | override suspend fun convert(result: KtorfitResult): Any { 20 | when (result) { 21 | is KtorfitResult.Success -> { 22 | val response = result.response 23 | val envelope = response.body() 24 | return envelope.user 25 | } 26 | 27 | is KtorfitResult.Failure -> { 28 | throw result.throwable 29 | } 30 | } 31 | } 32 | } 33 | } 34 | return null 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/CreateIssue.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.api.GithubService 4 | import com.example.api.createGithubService 5 | import de.jensklingenberg.ktorfit.ktorfit 6 | import io.ktor.client.* 7 | import io.ktor.client.plugins.contentnegotiation.* 8 | import io.ktor.serialization.kotlinx.json.* 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.runBlocking 11 | import kotlinx.serialization.json.Json 12 | 13 | fun main() { 14 | val jvmClient = 15 | HttpClient { 16 | install(ContentNegotiation) { 17 | json( 18 | Json { 19 | isLenient = true 20 | ignoreUnknownKeys = true 21 | } 22 | ) 23 | } 24 | expectSuccess = false 25 | } 26 | 27 | val jvmKtorfit = 28 | ktorfit { 29 | baseUrl(GithubService.baseUrl) 30 | httpClient(jvmClient) 31 | } 32 | 33 | val testApi = jvmKtorfit.createGithubService() 34 | 35 | runBlocking { 36 | testApi.listCommits("foso", "Experimental").collect { 37 | println(it.first().author) 38 | } 39 | 40 | // println( testApi.createIsseu(Issuedata("hey","ho"))) 41 | // BODY {"title":"title","body":"This is a test"} 42 | // BODY Issuedata(title=Hallo, body=hhhh) 43 | delay(3000) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Gradle 2 | ## Compile errors 3 | By default, Ktorfit will throw compile error when it finds conditions under which it can't ensure that it will work correct. 4 | You can set it in the Ktorfit config to change this 5 | 6 | ```kotlin 7 | ktorfit{ 8 | errorCheckingMode = ErrorCheckingMode.NONE 9 | } 10 | ``` 11 | 12 | You can set it in your build.gradle.kts file, 13 | 14 | * NONE : Turn off all Ktorfit related error checking 15 | 16 | * ERROR: Check for errors 17 | 18 | * WARNING: Turn errors into warnings 19 | 20 | ## QualifiedTypeName 21 | By default, Ktorfit will keep qualifiedTypename for TypeData in the generated code empty. You can set it in the Ktorfit config to change this: 22 | 23 | ```kotlin 24 | ktorfit { 25 | generateQualifiedTypeName = true 26 | } 27 | ``` 28 | 29 | ```kotlin title="Default code generation" 30 | ... 31 | val _typeData = TypeData.createTypeData( 32 | typeInfo = typeInfo>(), 33 | ) 34 | ... 35 | ``` 36 | 37 | ```kotlin title="With QualifiedTypeName true" 38 | ... 39 | val _typeData = TypeData.createTypeData( 40 | typeInfo = typeInfo>(), 41 | qualifiedTypename = "de.jensklingenberg.ktorfit.Call" 42 | ) 43 | ... 44 | ``` 45 | 46 | # Ktorfit Builder 47 | 48 | ## Add your own Ktor client 49 | You can set your Ktor client instance to the Ktorfit builder: 50 | 51 | ```kotlin 52 | val myClient = HttpClient() 53 | val ktorfit = Ktorfit.Builder().httpClient(myClient).build() 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/jvmTest/kotlin/de/jensklingenberg/ktorfit/BodyTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import de.jensklingenberg.ktorfit.http.Body 4 | import de.jensklingenberg.ktorfit.http.POST 5 | import io.ktor.client.HttpClient 6 | import io.ktor.client.request.HttpRequestData 7 | import io.ktor.content.TextContent 8 | import kotlinx.coroutines.runBlocking 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Assert.assertTrue 11 | import org.junit.Test 12 | 13 | interface BodyTestApi { 14 | @POST("example") 15 | suspend fun testBody( 16 | @Body body: String, 17 | ) 18 | } 19 | 20 | class BodyTest { 21 | @Test 22 | fun testBodyWithString() { 23 | val engine = 24 | object : TestEngine() { 25 | override fun getRequestData(data: HttpRequestData) { 26 | assertTrue((data.body is TextContent)) 27 | assertEquals("testBody", (data.body as TextContent).text) 28 | 29 | return 30 | } 31 | } 32 | 33 | try { 34 | val ktorfit = 35 | Ktorfit 36 | .Builder() 37 | .baseUrl("http://localhost/") 38 | .httpClient(HttpClient(engine)) 39 | .build() 40 | 41 | runBlocking { 42 | ktorfit.create(_BodyTestApiProvider()).testBody("testBody") 43 | } 44 | } catch (ex: Exception) { 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.nonFinalResIds=false 25 | #ksp.useKSP2=true -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/Pods/Local Podspecs/shared.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "version": "1.0", 4 | "homepage": "Link to the Shared Module homepage", 5 | "source": { 6 | "http": "" 7 | }, 8 | "authors": "", 9 | "license": "", 10 | "summary": "Some description for the Shared Module", 11 | "vendored_frameworks": "build/cocoapods/framework/shared.framework", 12 | "libraries": "c++", 13 | "platforms": { 14 | "ios": "14.1" 15 | }, 16 | "xcconfig": { 17 | "ENABLE_USER_SCRIPT_SANDBOXING": "NO" 18 | }, 19 | "pod_target_xcconfig": { 20 | "KOTLIN_PROJECT_PATH": ":shared", 21 | "PRODUCT_MODULE_NAME": "shared" 22 | }, 23 | "script_phases": [ 24 | { 25 | "name": "Build shared", 26 | "execution_position": "before_compile", 27 | "shell_path": "/bin/sh", 28 | "script": " if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"\"\n exit 0\n fi\n set -ev\n REPO_ROOT=\"$PODS_TARGET_SRCROOT\"\n \"$REPO_ROOT/../gradlew\" -p \"$REPO_ROOT\" $KOTLIN_PROJECT_PATH:syncFramework -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME -Pkotlin.native.cocoapods.archs=\"$ARCHS\" -Pkotlin.native.cocoapods.configuration=\"$CONFIGURATION\"\n" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/HeadersAnnotationsTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.tschuchort.compiletesting.SourceFile 4 | import com.tschuchort.compiletesting.kspSourcesDir 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Test 7 | import java.io.File 8 | 9 | class HeadersAnnotationsTest { 10 | @Test 11 | fun whenHeadersAnnotationFound_AddHeader() { 12 | val source = 13 | SourceFile.kotlin( 14 | "Source.kt", 15 | """ 16 | package com.example.api 17 | import de.jensklingenberg.ktorfit.http.GET 18 | import de.jensklingenberg.ktorfit.http.Headers 19 | 20 | interface TestService { 21 | 22 | @Headers(value = ["x:y","a:b"]) 23 | @GET("posts") 24 | suspend fun test(): String 25 | } 26 | """, 27 | ) 28 | 29 | val expectedHeadersArgumentText = 30 | "headers{\n" + 31 | " append(\"x\", \"y\")\n" + 32 | " append(\"a\", \"b\")\n" + 33 | " } " 34 | 35 | val compilation = getCompilation(listOf(source)) 36 | val result = compilation.compile() 37 | 38 | val generatedSourcesDir = compilation.kspSourcesDir 39 | val generatedFile = 40 | File( 41 | generatedSourcesDir, 42 | "/kotlin/com/example/api/_TestServiceImpl.kt", 43 | ) 44 | val actualSource = generatedFile.readText() 45 | assertTrue(actualSource.contains(expectedHeadersArgumentText)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | First do the [Installation](./installation.md) 2 | 3 | Let's say you want to make a GET Request to https://swapi.dev/api/people/1/ 4 | 5 | Create a new Kotlin interface: 6 | 7 | ```kotlin 8 | interface ExampleApi { 9 | @GET("people/1/") 10 | suspend fun getPerson(): String 11 | } 12 | ``` 13 | 14 | Now we add a function that will be used to make our request. The @GET annotation will tell Ktorfit that this a GET request. The value of @GET is the relative URL path that will be appended to the base url which we set later. 15 | 16 | An interface used for Ktorfit needs to have a HTTP method annotation on every function. 17 | Because Ktor relies on Coroutines by default your functions need to have the **suspend** modifier. Alternatively you can use [#Flow](../converters/responseconverter#flow) or [Call](../converters/responseconverter#call) 18 | 19 | !!! info 20 | 21 | The return type String will return the response text. When you want directly parse the response into a class you need to add a JSON,XML, etc. converter to Ktor 22 | 23 | ```kotlin 24 | val ktorfit = Ktorfit.Builder().baseUrl("https://swapi.dev/api/").build() 25 | val exampleApi = ktorfit.createExampleApi() 26 | ``` 27 | 28 | Next we use the Ktorfit builder to create a Ktorfit instance, and set the base url. 29 | After compiling the project we can then use the generated extension function to receive an implementation of the wanted type. 30 | 31 | ```kotlin 32 | val response = exampleApi.getPerson() 33 | println(response) 34 | ``` 35 | 36 | Now we can use exampleApi to make the request. -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/RequestConverterText.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import com.squareup.kotlinpoet.FunSpec 4 | import com.squareup.kotlinpoet.ksp.toClassName 5 | import de.jensklingenberg.ktorfit.model.ParameterData 6 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.RequestType 7 | import de.jensklingenberg.ktorfit.model.converterHelper 8 | 9 | fun FunSpec.Builder.addRequestConverterText(parameterDataList: List) = 10 | apply { 11 | if (parameterDataList.any { it.hasAnnotation() }) { 12 | parameterDataList.map { parameter -> 13 | val requestTypeClassName = 14 | parameter.annotations 15 | .filterIsInstance() 16 | .firstOrNull() 17 | ?.requestType 18 | ?.toClassName() 19 | if (parameter.hasAnnotation()) { 20 | this.addStatement( 21 | "val %L: %T = %L.convertParameterType(%L,%L::class,%T::class)", 22 | parameter.name, 23 | requestTypeClassName!!, 24 | converterHelper.objectName, 25 | parameter.name, 26 | parameter.name, 27 | requestTypeClassName, 28 | ) 29 | } 30 | parameter.name 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/TagAnnotationsTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.tschuchort.compiletesting.SourceFile 4 | import com.tschuchort.compiletesting.kspSourcesDir 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Test 7 | import java.io.File 8 | 9 | class TagAnnotationsTest { 10 | @Test 11 | fun whenTagsAnnotationFound_AddThemAsAttributeKey() { 12 | val source = 13 | SourceFile.kotlin( 14 | "Source.kt", 15 | """ 16 | package com.example.api 17 | import de.jensklingenberg.ktorfit.http.GET 18 | import de.jensklingenberg.ktorfit.http.Tag 19 | 20 | interface TestService { 21 | @GET("posts") 22 | suspend fun test(@Tag myTag1 : String, @Tag("myTag2") someParameter: Int?): String 23 | } 24 | """, 25 | ) 26 | 27 | val expectedHeadersArgumentText = 28 | """attributes.put(AttributeKey("myTag1"), myTag1) 29 | someParameter?.let{ attributes.put(AttributeKey("myTag2"), it) }""" 30 | 31 | val compilation = getCompilation(listOf(source)) 32 | println(compilation.languageVersion) 33 | val result = compilation.compile() 34 | 35 | val generatedSourcesDir = compilation.kspSourcesDir 36 | val generatedFile = 37 | File( 38 | generatedSourcesDir, 39 | "/kotlin/com/example/api/_TestServiceImpl.kt", 40 | ) 41 | 42 | val actualSource = generatedFile.readText() 43 | assertTrue(actualSource.contains(expectedHeadersArgumentText)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | #Installation 2 | 3 | ## Setup 4 | (You can also look how it's done in the [examples](https://github.com/Foso/Ktorfit/tree/master/example)) 5 | 6 | #### Gradle Plugins 7 | You need to add KSP and the [Ktorfit Gradle plugin](https://plugins.gradle.org/plugin/de.jensklingenberg.ktorfit) 8 | ```kotlin 9 | plugins { 10 | id("com.google.devtools.ksp") version "CURRENT_KSP_VERSION" 11 | id("de.jensklingenberg.ktorfit") version "{{ktorfit.release}}" 12 | } 13 | ``` 14 | 15 | #### Ktorfit-lib 16 | 17 | Add the Ktorfit-lib to your common module. You can find all available versions [here](https://repo.maven.apache.org/maven2/de/jensklingenberg/ktorfit/ktorfit-lib/) 18 | ```kotlin 19 | val ktorfitVersion = "{{ktorfit.release}}" 20 | 21 | sourceSets { 22 | val commonMain by getting{ 23 | dependencies{ 24 | implementation("de.jensklingenberg.ktorfit:ktorfit-lib:$ktorfitVersion") 25 | } 26 | } 27 | ``` 28 | 29 | You can also use **"de.jensklingenberg.ktorfit:ktorfit-lib-light"** 30 | this will only add the Ktor client core dependency and not the platform dependencies for the clients. 31 | This will give you more control over the used clients, but you have to add them yourself. https://ktor.io/docs/http-client-engines.html 32 | Everything else is the same as "ktorfit-lib" 33 | 34 | #### Ktor 35 | Ktorfit is based on Ktor clients {{ktor.release}}. You don't need to add an extra dependency for the default clients. 36 | When you want to use Ktor plugins for things like serialization, you need to add the dependencies, and they need to be compatible with {{ktor.release}} 37 | 38 | 39 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.kotlin.dsl.maven 2 | 3 | pluginManagement { 4 | 5 | repositories { 6 | mavenLocal() 7 | google() 8 | 9 | mavenCentral() 10 | gradlePluginPortal() 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | mavenLocal() 16 | google() 17 | mavenCentral() 18 | // your repos 19 | } 20 | } 21 | resolutionStrategy { 22 | eachPlugin { 23 | if (requested.id.id == "de.jensklingenberg.ktorfit") { 24 | useModule("de.jensklingenberg.ktorfit:de.jensklingenberg.ktorfit.gradle.plugin:${requested.version}") 25 | } 26 | } 27 | } 28 | } 29 | 30 | dependencyResolutionManagement { 31 | repositories { 32 | mavenLocal() 33 | mavenCentral() 34 | google() 35 | maven { 36 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 37 | } 38 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") 39 | } 40 | } 41 | 42 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 43 | 44 | rootProject.name = "Ktorfit" 45 | include(":ktorfit-gradle-plugin") 46 | include(":ktorfit-ksp") 47 | 48 | // Only include sandbox if not running a publish task 49 | 50 | include(":ktorfit-compiler-plugin") 51 | include(":ktorfit-lib-core") 52 | include(":ktorfit-lib") 53 | include(":ktorfit-annotations") 54 | include(":ktorfit-converters:flow") 55 | include(":ktorfit-converters:call") 56 | include(":ktorfit-converters:response") 57 | include(":sandbox") 58 | -------------------------------------------------------------------------------- /.github/workflows/preview-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Preview documentation 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, closed] 6 | paths: 7 | - 'docs/**' 8 | - 'mkdocs.yml' 9 | - 'pyproject.toml' 10 | 11 | concurrency: 12 | group: gh-pages-deploy 13 | cancel-in-progress: false 14 | 15 | jobs: 16 | preview_docs: 17 | if: github.event.pull_request.head.repo.full_name == github.repository 18 | runs-on: ubuntu-latest 19 | env: 20 | TERM: dumb 21 | 22 | steps: 23 | - uses: actions/checkout@v6 24 | with: 25 | submodules: "recursive" 26 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 27 | - name: Setup Python 28 | uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 29 | with: 30 | python-version: '3.14' 31 | architecture: 'x64' 32 | - name: Install dependencies 33 | run: | 34 | python3 -m pip install --upgrade pip # install pip 35 | python3 -m pip install mkdocs # install mkdocs 36 | python3 -m pip install mkdocs-material # install material theme 37 | python3 -m pip install mkdocs-git-revision-date-localized-plugin 38 | python3 -m pip install mkdocs-minify-plugin 39 | python3 -m pip install mkdocs-macros-plugin 40 | 41 | - name: Build documentation 42 | if: github.event.action != 'closed' 43 | run: mkdocs build 44 | 45 | - name: Deploy PR preview 46 | uses: rossjrw/pr-preview-action@v1 47 | with: 48 | source-dir: ./site/ 49 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/test/kotlin/de/jensklingenberg/ktorfit/ReturnTypeDataTest.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.tschuchort.compiletesting.SourceFile 4 | import com.tschuchort.compiletesting.kspSourcesDir 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Test 7 | import java.io.File 8 | 9 | class ReturnTypeDataTest { 10 | @Test 11 | fun testFunctionWithBody() { 12 | val source = 13 | SourceFile.kotlin( 14 | "Source.kt", 15 | """ 16 | package com.example.api 17 | import de.jensklingenberg.ktorfit.http.POST 18 | import de.jensklingenberg.ktorfit.http.Body 19 | 20 | interface TestService { 21 | @POST("user") 22 | suspend fun test(@Body id: String): Map 23 | } 24 | """, 25 | ) 26 | 27 | val expectedBodyDataArgumentText = 28 | """val _ext: HttpRequestBuilder.() -> Unit = { 29 | this.method = HttpMethod.parse("POST") 30 | url{ 31 | takeFrom(_baseUrl + "user") 32 | } 33 | setBody(id) 34 | }""" 35 | 36 | val compilation = getCompilation(listOf(source)) 37 | val result = compilation.compile() 38 | 39 | val generatedSourcesDir = compilation.kspSourcesDir 40 | val generatedFile = 41 | File( 42 | generatedSourcesDir, 43 | "/kotlin/com/example/api/_TestServiceImpl.kt", 44 | ) 45 | assertTrue(generatedFile.exists()) 46 | 47 | val actualSource = generatedFile.readText() 48 | assertTrue(actualSource.contains(expectedBodyDataArgumentText)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/poetspec/Utils.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.poetspec 2 | 3 | import com.google.devtools.ksp.symbol.KSType 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.TypeName 6 | import com.squareup.kotlinpoet.ksp.toTypeName 7 | import java.io.File 8 | 9 | fun findTypeName( 10 | ksType: KSType, 11 | filePath: String 12 | ): TypeName = 13 | if (ksType.isError) { 14 | val className = 15 | ksType 16 | .toString() 17 | .substringAfter("") 19 | findTypeImport(className, filePath) 20 | ?: throw IllegalStateException("Import for $ksType not found") 21 | } else { 22 | ksType.toTypeName() 23 | } 24 | 25 | /** 26 | * This is needed because since Kotlin 2.0 KSP can't resolve a type that is not in the same module 27 | * So the only way to get the type is to read the file and find the import in case of an error type 28 | * This approach is not perfect because it won't work when wildcard imports are used. 29 | * Also reading the source file is slow and should be avoided if possible 30 | */ 31 | private fun findTypeImport( 32 | className: String, 33 | filePath: String, 34 | ): ClassName? { 35 | val file = File(filePath) 36 | val lines = file.readLines() 37 | val imports = lines.filter { it.startsWith("import") }.map { it.substringAfter("import ") } 38 | val classImport = imports.firstOrNull { it.endsWith(className) } 39 | return classImport?.let { ClassName(classImport.substringBeforeLast("."), className) } 40 | } 41 | -------------------------------------------------------------------------------- /example/MultiplatformExample/shared/src/commonMain/kotlin/com/example/ktorfittest/Greeting.kt: -------------------------------------------------------------------------------- 1 | package com.example.ktorfittest 2 | 3 | import de.jensklingenberg.ktorfit.converter.CallConverterFactory 4 | import de.jensklingenberg.ktorfit.converter.FlowConverterFactory 5 | import de.jensklingenberg.ktorfit.ktorfit 6 | import io.ktor.client.* 7 | import io.ktor.client.plugins.contentnegotiation.* 8 | import io.ktor.serialization.kotlinx.json.* 9 | import kotlinx.coroutines.DelicateCoroutinesApi 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.launch 12 | import kotlinx.serialization.json.Json 13 | 14 | val ktorfit = 15 | ktorfit { 16 | baseUrl(StarWarsApi.baseUrl) 17 | httpClient( 18 | HttpClient { 19 | install(ContentNegotiation) { 20 | json( 21 | Json { 22 | isLenient = true 23 | ignoreUnknownKeys = true 24 | } 25 | ) 26 | } 27 | } 28 | ) 29 | converterFactories( 30 | FlowConverterFactory(), 31 | CallConverterFactory() 32 | ) 33 | } 34 | 35 | val starWarsApi = ktorfit.create() 36 | 37 | class Greeting { 38 | fun greeting(): String { 39 | loadData() 40 | return "Hello, ${Platform().platform}! Look in the LogCat" 41 | } 42 | } 43 | 44 | @OptIn(DelicateCoroutinesApi::class) 45 | fun loadData() { 46 | GlobalScope.launch { 47 | val response = starWarsApi.getPersonByIdResponse(3) 48 | println("Ktorfit:" + Platform().platform + ":" + response) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/JvmPlaceHolderApi.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.api.Response 4 | import com.example.api.StarWarsApi 5 | import com.example.model.People 6 | import de.jensklingenberg.ktorfit.Call 7 | import de.jensklingenberg.ktorfit.http.* 8 | import io.ktor.client.statement.* 9 | 10 | internal interface JvmPlaceHolderApi : StarWarsApi { 11 | @GET("people/{id}/") 12 | suspend fun getPersonById2( 13 | @Path("id") peopleId: Int 14 | ): People 15 | 16 | @GET("people/{id}/") 17 | suspend fun testQuery( 18 | @Path("id") peopleId: Int, 19 | @Query world: String? = "World" 20 | ): People 21 | 22 | @GET("people/{id}/") 23 | suspend fun testQueryName( 24 | @Path("id") peopleId: Int, 25 | @QueryName na: List? 26 | ): People 27 | 28 | @GET("people/{id}/") 29 | suspend fun testQueryName2( 30 | @Path("id") peopleId: Int, 31 | @QueryName na: Map?, 32 | @QueryMap na2: Map? 33 | ): People 34 | 35 | @Streaming 36 | @GET("people/1/") 37 | suspend fun getPostsStreaming(): HttpStatement 38 | 39 | @GET("people/{id}/") 40 | fun getPersonById2AsResponse( 41 | @Path("id") peopleId: Int 42 | ): Response 43 | 44 | @Headers(value = ["Content-Type: application/json"]) 45 | @GET("people/{id}/") 46 | suspend fun callPersonById2AsResponse( 47 | @Path("id") peopleId: Int 48 | ): Call> 49 | 50 | @GET() 51 | suspend fun getPersonByIdByUrl( 52 | @Url peopleId: String, 53 | @QueryMap name: Map? 54 | ): People 55 | } 56 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | 28 | UIRequiredDeviceCapabilities 29 | 30 | armv7 31 | 32 | UISupportedInterfaceOrientations 33 | 34 | UIInterfaceOrientationPortrait 35 | UIInterfaceOrientationLandscapeLeft 36 | UIInterfaceOrientationLandscapeRight 37 | 38 | UISupportedInterfaceOrientations~ipad 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationPortraitUpsideDown 42 | UIInterfaceOrientationLandscapeLeft 43 | UIInterfaceOrientationLandscapeRight 44 | 45 | UILaunchScreen 46 | 47 | 48 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonTest/kotlin/de/jensklingenberg/ktorfit/TestEngine.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import io.ktor.client.engine.HttpClientEngine 4 | import io.ktor.client.engine.HttpClientEngineBase 5 | import io.ktor.client.engine.HttpClientEngineConfig 6 | import io.ktor.client.request.HttpRequestData 7 | import io.ktor.client.request.HttpResponseData 8 | import io.ktor.http.Headers 9 | import io.ktor.http.HttpProtocolVersion 10 | import io.ktor.http.HttpStatusCode 11 | import io.ktor.util.date.GMTDate 12 | import io.ktor.utils.io.InternalAPI 13 | import kotlinx.coroutines.CoroutineDispatcher 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.Job 16 | import kotlin.coroutines.CoroutineContext 17 | 18 | open class TestEngine : HttpClientEngineBase("ktor-mock") { 19 | override val config: HttpClientEngineConfig 20 | get() = HttpClientEngineConfig() 21 | override val dispatcher: CoroutineDispatcher 22 | get() = Dispatchers.Default 23 | 24 | private suspend fun HttpClientEngine.createCallContext(parentJob: Job): CoroutineContext { 25 | val callJob = Job(parentJob) 26 | val callContext = coroutineContext + callJob 27 | 28 | return callContext 29 | } 30 | 31 | @InternalAPI 32 | override suspend fun execute(data: HttpRequestData): HttpResponseData { 33 | val coroutineContext1 = createCallContext(data.executionContext) 34 | getRequestData(data) 35 | return HttpResponseData( 36 | HttpStatusCode.Accepted, 37 | GMTDate(), 38 | Headers.Empty, 39 | HttpProtocolVersion.HTTP_2_0, 40 | "", 41 | coroutineContext1, 42 | ) 43 | } 44 | 45 | open fun getRequestData(data: HttpRequestData) {} 46 | } 47 | -------------------------------------------------------------------------------- /ktorfit-converters/flow/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/FlowConverterFactory.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import io.ktor.client.call.body 5 | import io.ktor.client.statement.HttpResponse 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | 9 | /** 10 | * Factory that enables the use of Flow as return type 11 | */ 12 | public class FlowConverterFactory : Converter.Factory { 13 | override fun responseConverter( 14 | typeData: TypeData, 15 | ktorfit: Ktorfit, 16 | ): Converter.ResponseConverter? { 17 | if (typeData.typeInfo.type == Flow::class) { 18 | return object : Converter.ResponseConverter> { 19 | override fun convert(getResponse: suspend () -> HttpResponse): Flow { 20 | val requestType = typeData.typeArgs.first() 21 | return flow { 22 | val response = getResponse() 23 | if (requestType.typeInfo.type == HttpResponse::class) { 24 | emit(response) 25 | } else { 26 | val convertedBody = 27 | ktorfit.nextSuspendResponseConverter( 28 | this@FlowConverterFactory, 29 | typeData.typeArgs.first(), 30 | )?.convert(KtorfitResult.Success(response)) 31 | ?: response.body(typeData.typeArgs.first().typeInfo) 32 | emit(convertedBody) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | return null 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ktorfit-converters/response/api/jvm/response.api: -------------------------------------------------------------------------------- 1 | public final class de/jensklingenberg/ktorfit/Response { 2 | public static final field Companion Lde/jensklingenberg/ktorfit/Response$Companion; 3 | public synthetic fun (Lio/ktor/client/statement/HttpResponse;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/internal/DefaultConstructorMarker;)V 4 | public final fun body ()Ljava/lang/Object; 5 | public final fun errorBody ()Ljava/lang/Object; 6 | public final fun getCode ()I 7 | public final fun getHeaders ()Lio/ktor/http/Headers; 8 | public final fun getMessage ()Ljava/lang/String; 9 | public final fun getStatus ()Lio/ktor/http/HttpStatusCode; 10 | public final fun isSuccessful ()Z 11 | public final fun raw ()Lio/ktor/client/statement/HttpResponse; 12 | public fun toString ()Ljava/lang/String; 13 | } 14 | 15 | public final class de/jensklingenberg/ktorfit/Response$Companion { 16 | public final fun error (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)Lde/jensklingenberg/ktorfit/Response; 17 | public final fun success (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)Lde/jensklingenberg/ktorfit/Response; 18 | } 19 | 20 | public final class de/jensklingenberg/ktorfit/converter/ResponseConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 21 | public fun ()V 22 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 23 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 24 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /example/AndroidOnlyExample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /ktorfit-converters/response/api/android/response.api: -------------------------------------------------------------------------------- 1 | public final class de/jensklingenberg/ktorfit/Response { 2 | public static final field Companion Lde/jensklingenberg/ktorfit/Response$Companion; 3 | public synthetic fun (Lio/ktor/client/statement/HttpResponse;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/internal/DefaultConstructorMarker;)V 4 | public final fun body ()Ljava/lang/Object; 5 | public final fun errorBody ()Ljava/lang/Object; 6 | public final fun getCode ()I 7 | public final fun getHeaders ()Lio/ktor/http/Headers; 8 | public final fun getMessage ()Ljava/lang/String; 9 | public final fun getStatus ()Lio/ktor/http/HttpStatusCode; 10 | public final fun isSuccessful ()Z 11 | public final fun raw ()Lio/ktor/client/statement/HttpResponse; 12 | public fun toString ()Ljava/lang/String; 13 | } 14 | 15 | public final class de/jensklingenberg/ktorfit/Response$Companion { 16 | public final fun error (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)Lde/jensklingenberg/ktorfit/Response; 17 | public final fun success (Ljava/lang/Object;Lio/ktor/client/statement/HttpResponse;)Lde/jensklingenberg/ktorfit/Response; 18 | } 19 | 20 | public final class de/jensklingenberg/ktorfit/converter/ResponseConverterFactory : de/jensklingenberg/ktorfit/converter/Converter$Factory { 21 | public fun ()V 22 | public fun requestParameterConverter (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lde/jensklingenberg/ktorfit/converter/Converter$RequestParameterConverter; 23 | public fun responseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$ResponseConverter; 24 | public fun suspendResponseConverter (Lde/jensklingenberg/ktorfit/converter/TypeData;Lde/jensklingenberg/ktorfit/Ktorfit;)Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /docs/suspendresponseconverter.md: -------------------------------------------------------------------------------- 1 | !!! warning "SuspendResponseConverter is deprecated, use [Converter.SuspendResponseConverter](./converters/suspendresponseconverter.md) instead" 2 | 3 | Because Ktor relies on Coroutines by default your functions need to have the suspend modifier. 4 | 5 | To change this, you need to use a SuspendResponseConverter, you add your own or use [Flow](#flow) or [Call](#call) 6 | 7 | You can add RequestConverter on your Ktorfit object. 8 | 9 | ```kotlin 10 | ktorfit.responseConverter(FlowResponseConverter()) 11 | ``` 12 | 13 | ### Flow 14 | Ktorfit has support for Kotlin Flow. You need add the FlowResponseConverter() to your Ktorfit instance. 15 | 16 | ```kotlin 17 | ktorfit.responseConverter(FlowResponseConverter()) 18 | ``` 19 | 20 | ```kotlin 21 | 22 | @GET("comments") 23 | fun getCommentsById(@Query("postId") postId: String): Flow> 24 | ``` 25 | 26 | Then you can drop the **suspend** modifier and wrap your return type with Flow<> 27 | 28 | 29 | ### Call 30 | 31 | ```kotlin 32 | ktorfit.responseConverter(CallResponseConverter()) 33 | ``` 34 | ```kotlin 35 | @GET("people/{id}/") 36 | fun getPersonById(@Path("id") peopleId: Int): Call 37 | ``` 38 | 39 | ```kotlin 40 | exampleApi.getPersonById(3).onExecute(object : Callback{ 41 | override fun onResponse(call: People, response: HttpResponse) { 42 | //Do something with Response 43 | } 44 | 45 | override fun onError(exception: Exception) { 46 | //Do something with exception 47 | } 48 | }) 49 | ``` 50 | 51 | You can use Call to receive the response in a Callback. 52 | 53 | ### Your own 54 | You can also add your own Converter. You just need to implement RequestConverter. Inside the converter you need to handle 55 | the conversion from **suspend** to your async code. 56 | 57 | ```kotlin 58 | class MyOwnResponseConverter : SuspendResponseConverter { 59 | ... 60 | ``` 61 | -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/src/main/java/de/jensklingenberg/ktorfit/ExampleCommandLineProcessor.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import com.google.auto.service.AutoService 4 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 5 | import org.jetbrains.kotlin.compiler.plugin.CliOption 6 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CompilerConfiguration 9 | import org.jetbrains.kotlin.config.CompilerConfigurationKey 10 | 11 | @OptIn(ExperimentalCompilerApi::class) 12 | @AutoService(CommandLineProcessor::class) // don't forget! 13 | class ExampleCommandLineProcessor : CommandLineProcessor { 14 | override val pluginId: String = "ktorfitPlugin" 15 | 16 | override val pluginOptions: Collection = 17 | listOf( 18 | CliOption( 19 | optionName = "enabled", 20 | valueDescription = "", 21 | description = "whether to enable the plugin or not", 22 | ), 23 | CliOption( 24 | optionName = "logging", 25 | valueDescription = "", 26 | description = "whether to enable logging", 27 | ), 28 | ) 29 | 30 | override fun processOption( 31 | option: AbstractCliOption, 32 | value: String, 33 | configuration: CompilerConfiguration, 34 | ) = when (option.optionName) { 35 | "enabled" -> configuration.put(KEY_ENABLED, value.toBoolean()) 36 | "logging" -> configuration.put(KEY_LOGGING, value.toBoolean()) 37 | else -> configuration.put(KEY_ENABLED, true) 38 | } 39 | } 40 | 41 | val KEY_ENABLED = CompilerConfigurationKey("whether the plugin is enabled") 42 | val KEY_LOGGING = CompilerConfigurationKey("whether logging is enabled") 43 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/UrlCodeGeneration.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import de.jensklingenberg.ktorfit.model.ParameterData 4 | import de.jensklingenberg.ktorfit.model.annotations.HttpMethodAnnotation 5 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Path 6 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation.Url 7 | 8 | fun getUrlCode( 9 | params: List, 10 | methodAnnotation: HttpMethodAnnotation, 11 | queryCode: String, 12 | ): String { 13 | var urlPath = 14 | methodAnnotation.path.ifEmpty { 15 | params 16 | .firstOrNull { it.hasAnnotation() } 17 | ?.let { 18 | "\${${it.name}}" 19 | }.orEmpty() 20 | } 21 | 22 | val baseUrl = 23 | if (methodAnnotation.path.startsWith("http")) { 24 | "" 25 | } else { 26 | params.firstOrNull { it.hasAnnotation() }?.let { parameterData -> 27 | "(_baseUrl.takeIf{ !${parameterData.name}.startsWith(\"http\")} ?: \"\") + " 28 | } ?: "_baseUrl + " 29 | } 30 | 31 | params.filter { it.hasAnnotation() }.forEach { parameterData -> 32 | val paramName = parameterData.name 33 | val pathAnnotation = 34 | parameterData.findAnnotationOrNull() 35 | ?: throw IllegalStateException("Path annotation not found") 36 | 37 | val pathEncoded = 38 | if (!pathAnnotation.encoded) { 39 | ".encodeURLPath()" 40 | } else { 41 | "" 42 | } 43 | urlPath = urlPath.replace("{${pathAnnotation.value}}", "\${\"\$${paramName}\"$pathEncoded}") 44 | } 45 | 46 | return "url{\ntakeFrom(${baseUrl}\"$urlPath\")\n" + queryCode + "}" 47 | } 48 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 |

Ktorfit

2 | 3 | [![Maven](https://img.shields.io/badge/Maven-Central-download.svg?style=flat-square)](https://central.sonatype.com/search?q=g:de.jensklingenberg.ktorfit) 4 | [![License](https://img.shields.io/badge/Apache-2.0-green.svg)](https://github.com/Foso/Ktorfit/blob/master/LICENSE) 5 | 6 |

7 | 8 |

9 | 10 | # Introduction 11 | 12 | Ktorfit is an HTTP client/Kotlin Symbol Processor for Kotlin Multiplatform (Js, Jvm, Android, iOS, Linux) 13 | using [KSP](https://github.com/google/ksp) and [Ktor clients](https://ktor.io/docs/getting-started-ktor-client.html) 14 | inspired by [Retrofit](https://square.github.io/retrofit/) 15 | 16 | ## Compatibility 17 | 18 | | Ktorfit-version | 19 | |-------------------------------------------------------------------------------| 20 | | **_2.6.0_** https://github.com/Foso/Ktorfit/blob/master/docs/CHANGELOG.md#260 | 21 | | **_2.5.0_** https://github.com/Foso/Ktorfit/blob/master/docs/CHANGELOG.md#250 | 22 | 23 | # Installation 24 | 25 | Please see [Installation](./installation.md) 26 | 27 | # Quick start 28 | 29 | Please see [Quick start](./quick-start.md) 30 | 31 | ## Requests 32 | 33 | See [Requests](./requests.md) 34 | 35 | ## Converters 36 | 37 | See documentation [Here](./converters/converters.md) 38 | 39 | ## Changelog 40 | 41 | See [changelog](./CHANGELOG.md) 42 | 43 | ## Acknowledgments 44 | 45 | Some parts of this project are reusing ideas that are originally coming 46 | from [Retrofit](https://square.github.io/retrofit/) from [Square](https://github.com/square). Thank you for Retrofit! 47 | 48 | Thanks to JetBrains for Ktor and Kotlin! 49 | 50 | ## Contributions 51 | 52 | When you find unexpected behaviour please write an [issue](https://github.com/Foso/Ktorfit/issues/new/choose) 53 | -------------------------------------------------------------------------------- /ktorfit-converters/response/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/ResponseClassSuspendConverter.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import de.jensklingenberg.ktorfit.Response 5 | import io.ktor.client.call.body 6 | import io.ktor.client.statement.HttpResponse 7 | 8 | internal class ResponseClassSuspendConverter(private val typeData: TypeData, private val ktorfit: Ktorfit) : 9 | Converter.SuspendResponseConverter> { 10 | override suspend fun convert(result: KtorfitResult): Response { 11 | return when (result) { 12 | is KtorfitResult.Success -> { 13 | val typeInfo = typeData.typeArgs.first().typeInfo 14 | val rawResponse = result.response 15 | val code: Int = rawResponse.status.value 16 | when { 17 | code < 200 || code >= 300 -> { 18 | val errorBody = rawResponse.body() 19 | Response.error(errorBody, rawResponse) 20 | } 21 | 22 | code == 204 || code == 205 -> { 23 | Response.success(null, rawResponse) 24 | } 25 | 26 | else -> { 27 | val convertedBody = 28 | ktorfit.nextSuspendResponseConverter( 29 | null, 30 | typeData.typeArgs.first(), 31 | )?.convert(result) 32 | ?: rawResponse.body(typeInfo) 33 | Response.success(convertedBody, rawResponse) 34 | } 35 | } 36 | } 37 | 38 | is KtorfitResult.Failure -> { 39 | throw result.throwable 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/responseconverter.md: -------------------------------------------------------------------------------- 1 | !!! warning "ResponseConverter is deprecated, use [Converter.ResponseConverter](./converters/responseconverter.md) instead" 2 | 3 | Let`s say you have a function that requests a list of comments 4 | 5 | ```kotlin 6 | @GET("posts/{postId}/comments") 7 | suspend fun getCommentsByPostId(@Path("postId") postId: Int): List 8 | ``` 9 | 10 | But now you want to directly wrap your comment list in your data holder class e.g. "MyOwnResponse" 11 | 12 | ```kotlin 13 | sealed class MyOwnResponse { 14 | data class Success(val data: T) : MyOwnResponse() 15 | class Error(val ex:Throwable) : MyOwnResponse() 16 | 17 | companion object { 18 | fun success(data: T) = Success(data) 19 | fun error(ex: Throwable) = Error(ex) 20 | } 21 | } 22 | ``` 23 | 24 | To enable that, you have to implement a ResponseConverter. This class will be used to wrap the Ktor response 25 | inside your wrapper class. 26 | 27 | ```kotlin 28 | class MyOwnResponseConverter : ResponseConverter { 29 | 30 | override suspend fun wrapResponse( 31 | typeData: TypeData, 32 | requestFunction: suspend () -> Pair, 33 | ktorfit: Ktorfit 34 | ): Any { 35 | return try { 36 | val (info, response) = requestFunction() 37 | MyOwnResponse.success(response.body(info)) 38 | } catch (ex: Throwable) { 39 | MyOwnResponse.error(ex) 40 | } 41 | } 42 | 43 | override fun supportedType(typeData: TypeData, isSuspend: Boolean): Boolean { 44 | return typeData.qualifiedName == "com.example.model.MyOwnResponse" 45 | } 46 | } 47 | ``` 48 | 49 | You can then add the ResponseConverter on your Ktorfit object. 50 | 51 | ```kotlin 52 | ktorfit.responseConverter(MyOwnResponseConverter()) 53 | ``` 54 | 55 | Now add MyOwnResponse to your function 56 | ```kotlin 57 | @GET("posts/{postId}/comments") 58 | suspend fun getCommentsByPostId(@Path("postId") postId: Int): MyOwnResponse> 59 | ``` 60 | -------------------------------------------------------------------------------- /example/MultiplatformExample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "scale": "2x", 6 | "size": "20x20" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "scale": "3x", 11 | "size": "20x20" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "scale": "2x", 16 | "size": "29x29" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "scale": "3x", 21 | "size": "29x29" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "scale": "2x", 26 | "size": "40x40" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "scale": "3x", 31 | "size": "40x40" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "scale": "2x", 36 | "size": "60x60" 37 | }, 38 | { 39 | "idiom": "iphone", 40 | "scale": "3x", 41 | "size": "60x60" 42 | }, 43 | { 44 | "idiom": "ipad", 45 | "scale": "1x", 46 | "size": "20x20" 47 | }, 48 | { 49 | "idiom": "ipad", 50 | "scale": "2x", 51 | "size": "20x20" 52 | }, 53 | { 54 | "idiom": "ipad", 55 | "scale": "1x", 56 | "size": "29x29" 57 | }, 58 | { 59 | "idiom": "ipad", 60 | "scale": "2x", 61 | "size": "29x29" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "scale": "1x", 66 | "size": "40x40" 67 | }, 68 | { 69 | "idiom": "ipad", 70 | "scale": "2x", 71 | "size": "40x40" 72 | }, 73 | { 74 | "idiom": "ipad", 75 | "scale": "1x", 76 | "size": "76x76" 77 | }, 78 | { 79 | "idiom": "ipad", 80 | "scale": "2x", 81 | "size": "76x76" 82 | }, 83 | { 84 | "idiom": "ipad", 85 | "scale": "2x", 86 | "size": "83.5x83.5" 87 | }, 88 | { 89 | "idiom": "ios-marketing", 90 | "scale": "1x", 91 | "size": "1024x1024" 92 | } 93 | ], 94 | "info": { 95 | "author": "xcode", 96 | "version": 1 97 | } 98 | } -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/api/ktorfit-compiler-plugin.api: -------------------------------------------------------------------------------- 1 | public final class de/jensklingenberg/ktorfit/CommonCompilerPluginRegistrar : org/jetbrains/kotlin/compiler/plugin/CompilerPluginRegistrar { 2 | public static final field Companion Lde/jensklingenberg/ktorfit/CommonCompilerPluginRegistrar$Companion; 3 | public static final field PLUGIN_ID Ljava/lang/String; 4 | public fun ()V 5 | public fun getPluginId ()Ljava/lang/String; 6 | public fun getSupportsK2 ()Z 7 | public fun registerExtensions (Lorg/jetbrains/kotlin/compiler/plugin/CompilerPluginRegistrar$ExtensionStorage;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 8 | } 9 | 10 | public final class de/jensklingenberg/ktorfit/CommonCompilerPluginRegistrar$Companion { 11 | } 12 | 13 | public final class de/jensklingenberg/ktorfit/ExampleCommandLineProcessor : org/jetbrains/kotlin/compiler/plugin/CommandLineProcessor { 14 | public fun ()V 15 | public fun appendList (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;Ljava/lang/Object;)V 16 | public fun appendList (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Lorg/jetbrains/kotlin/config/CompilerConfigurationKey;Ljava/util/List;)V 17 | public fun applyOptionsFrom (Lorg/jetbrains/kotlin/config/CompilerConfiguration;Ljava/util/Map;Ljava/util/Collection;)V 18 | public fun getPluginId ()Ljava/lang/String; 19 | public fun getPluginOptions ()Ljava/util/Collection; 20 | public fun processOption (Lorg/jetbrains/kotlin/compiler/plugin/AbstractCliOption;Ljava/lang/String;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 21 | public fun processOption (Lorg/jetbrains/kotlin/compiler/plugin/CliOption;Ljava/lang/String;Lorg/jetbrains/kotlin/config/CompilerConfiguration;)V 22 | } 23 | 24 | public final class de/jensklingenberg/ktorfit/ExampleCommandLineProcessorKt { 25 | public static final fun getKEY_ENABLED ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; 26 | public static final fun getKEY_LOGGING ()Lorg/jetbrains/kotlin/config/CompilerConfigurationKey; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /ktorfit-lib-core/src/commonMain/kotlin/de/jensklingenberg/ktorfit/converter/builtin/DefaultSuspendResponseConverterFactory.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.converter.builtin 2 | 3 | import de.jensklingenberg.ktorfit.Ktorfit 4 | import de.jensklingenberg.ktorfit.converter.Converter 5 | import de.jensklingenberg.ktorfit.converter.KtorfitResult 6 | import de.jensklingenberg.ktorfit.converter.TypeData 7 | import io.ktor.client.statement.HttpResponse 8 | 9 | /** 10 | * Will be used when no other suspend converter was found 11 | * It is automatically applied last 12 | */ 13 | internal class DefaultSuspendResponseConverterFactory : Converter.Factory { 14 | class DefaultSuspendResponseConverter( 15 | val typeData: TypeData 16 | ) : Converter.SuspendResponseConverter { 17 | override suspend fun convert(result: KtorfitResult): Any? = 18 | when (result) { 19 | is KtorfitResult.Failure -> { 20 | if (typeData.isNullable) { 21 | null 22 | } else { 23 | throw result.throwable 24 | } 25 | } 26 | 27 | is KtorfitResult.Success -> { 28 | result.response.call.body(typeData.typeInfo) 29 | } 30 | } 31 | } 32 | 33 | class DefaultResponseConverter : Converter.ResponseConverter { 34 | override fun convert(getResponse: suspend () -> HttpResponse): Any? = null 35 | } 36 | 37 | override fun suspendResponseConverter( 38 | typeData: TypeData, 39 | ktorfit: Ktorfit, 40 | ): Converter.SuspendResponseConverter = DefaultSuspendResponseConverter(typeData) 41 | 42 | override fun responseConverter( 43 | typeData: TypeData, 44 | ktorfit: Ktorfit, 45 | ): Converter.ResponseConverter? = 46 | if (typeData.isNullable) { 47 | DefaultResponseConverter() 48 | } else { 49 | null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /sandbox/src/jvmMain/kotlin/de/jensklingenberg/ktorfit/demo/uploadFile.txt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.demo 2 | 3 | import com.example.api.GithubService 4 | import com.example.api.StarWarsApi 5 | import de.jensklingenberg.ktorfit.adapter.FlowCallAdapter 6 | import de.jensklingenberg.ktorfit.Ktorfit 7 | import de.jensklingenberg.ktorfit.adapter.KtorfitCallAdapter 8 | import de.jensklingenberg.ktorfit.create 9 | import io.ktor.client.* 10 | import io.ktor.client.plugins.* 11 | import io.ktor.client.request.forms.* 12 | import io.ktor.http.* 13 | import io.ktor.serialization.kotlinx.json.* 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.runBlocking 16 | import kotlinx.serialization.json.Json 17 | import java.io.File 18 | 19 | 20 | val jvmClient = HttpClient() { 21 | install(ContentNegotiation) { 22 | // register(ContentType.Application.Any, CustomJsonConverter()) 23 | // json(Json { isLenient = true; ignoreUnknownKeys = true }) 24 | } 25 | install(HttpTimeout) { 26 | 27 | } 28 | expectSuccess = false 29 | 30 | 31 | } 32 | 33 | val jvmKtorfit = Ktorfit(baseUrl = "http://localhost:8080/", jvmClient) 34 | 35 | val githubApi = jvmKtorfit.create() 36 | 37 | 38 | val starWarsApi = jvmKtorfit.create() 39 | 40 | 41 | fun main() { 42 | 43 | jvmKtorfit.addAdapter(FlowCallAdapter()) 44 | jvmKtorfit.addAdapter(RxCallAdapter()) 45 | jvmKtorfit.addAdapter(KtorfitCallAdapter()) 46 | 47 | 48 | runBlocking { 49 | // println(de.jensklingenberg.ktorfit.demo.getStarWarsApi.getPersonById(1).name) 50 | 51 | val test = githubApi.upload("Ktor logo", formData { 52 | append("image", File("ktor_logo.png").readBytes(), Headers.build { 53 | append(HttpHeaders.ContentType, "image/png") 54 | append(HttpHeaders.ContentDisposition, "filename=ktor_logo.png") 55 | }) 56 | }) 57 | 58 | // println(secondApi.getStream().get(1)) 59 | println(test) 60 | 61 | 62 | 63 | delay(3000) 64 | 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /ktorfit-compiler-plugin/src/main/java/de/jensklingenberg/ktorfit/ElementTransformer.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit 2 | 3 | import org.jetbrains.kotlin.backend.common.IrElementTransformerVoidWithContext 4 | import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext 5 | import org.jetbrains.kotlin.ir.IrStatement 6 | import org.jetbrains.kotlin.ir.declarations.IrProperty 7 | import org.jetbrains.kotlin.ir.declarations.IrValueParameter 8 | import org.jetbrains.kotlin.ir.declarations.IrVariable 9 | import org.jetbrains.kotlin.ir.expressions.IrCall 10 | import org.jetbrains.kotlin.ir.expressions.IrExpression 11 | import org.jetbrains.kotlin.ir.expressions.IrFunctionExpression 12 | 13 | internal class ElementTransformer( 14 | private val pluginContext: IrPluginContext, 15 | private val debugLogger: DebugLogger, 16 | ) : IrElementTransformerVoidWithContext() { 17 | override fun visitValueParameterNew(declaration: IrValueParameter): IrStatement { 18 | declaration.transform(CreateFuncTransformer(pluginContext, debugLogger), null) 19 | return super.visitValueParameterNew(declaration) 20 | } 21 | 22 | override fun visitPropertyNew(declaration: IrProperty): IrStatement { 23 | declaration.transform(CreateFuncTransformer(pluginContext, debugLogger), null) 24 | return super.visitPropertyNew(declaration) 25 | } 26 | 27 | override fun visitCall(expression: IrCall): IrExpression { 28 | expression.transform(CreateFuncTransformer(pluginContext, debugLogger), null) 29 | return super.visitCall(expression) 30 | } 31 | 32 | override fun visitVariable(declaration: IrVariable): IrStatement { 33 | declaration.transform(CreateFuncTransformer(pluginContext, debugLogger), null) 34 | return super.visitVariable(declaration) 35 | } 36 | 37 | override fun visitFunctionExpression(expression: IrFunctionExpression): IrExpression { 38 | expression.transform(CreateFuncTransformer(pluginContext, debugLogger), null) 39 | return super.visitFunctionExpression(expression) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ktorfit-ksp/src/main/kotlin/de/jensklingenberg/ktorfit/reqBuilderExtension/AttributesCodeGenerator.kt: -------------------------------------------------------------------------------- 1 | package de.jensklingenberg.ktorfit.reqBuilderExtension 2 | 3 | import com.squareup.kotlinpoet.AnnotationSpec 4 | import de.jensklingenberg.ktorfit.model.ParameterData 5 | import de.jensklingenberg.ktorfit.model.annotations.ParameterAnnotation 6 | import de.jensklingenberg.ktorfit.model.annotationsAttributeKey 7 | import de.jensklingenberg.ktorfit.utils.toClassName 8 | 9 | fun getAttributesCode( 10 | parameterDataList: List, 11 | annotations: List, 12 | ): String { 13 | val parameterAttributes = 14 | parameterDataList 15 | .filter { it.hasAnnotation() } 16 | .joinToString("\n") { 17 | val tag = 18 | it.findAnnotationOrNull() 19 | ?: throw IllegalStateException("Tag annotation not found") 20 | if (it.type.parameterType.isMarkedNullable) { 21 | "${it.name}?.let{ attributes.put(AttributeKey(\"${tag.value}\"), it) }" 22 | } else { 23 | "attributes.put(AttributeKey(\"${tag.value}\"), ${it.name})" 24 | } 25 | } 26 | 27 | if (annotations.isEmpty()) return parameterAttributes 28 | 29 | val annotationsAttribute = 30 | annotations.joinToString( 31 | separator = ",\n", 32 | prefix = "listOf(\n", 33 | postfix = ",\n)", 34 | ) { annotation -> 35 | annotation 36 | .members 37 | .joinToString { 38 | it.toString().replace("@", "") 39 | } 40 | .let { "${annotation.toClassName().simpleName}($it)" } 41 | } 42 | .let { "attributes.put(${annotationsAttributeKey.objectName}, $it)" } 43 | 44 | return if (parameterAttributes.isNotEmpty()) { 45 | parameterAttributes + "\n" + annotationsAttribute 46 | } else { 47 | annotationsAttribute 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sandbox/src/commonMain/kotlin/com/example/api/GithubService.kt: -------------------------------------------------------------------------------- 1 | package com.example.api 2 | 3 | import com.example.model.github.GithubFollowerResponseItem 4 | import com.example.model.github.Issuedata 5 | import com.example.model.github.TestReeeItem 6 | import de.jensklingenberg.ktorfit.Call 7 | import de.jensklingenberg.ktorfit.http.Body 8 | import de.jensklingenberg.ktorfit.http.GET 9 | import de.jensklingenberg.ktorfit.http.Header 10 | import de.jensklingenberg.ktorfit.http.Headers 11 | import de.jensklingenberg.ktorfit.http.POST 12 | import de.jensklingenberg.ktorfit.http.Path 13 | import kotlinx.coroutines.flow.Flow 14 | 15 | @Target(AnnotationTarget.FUNCTION) 16 | @Retention(AnnotationRetention.RUNTIME) 17 | annotation class CustomThrows 18 | 19 | interface GithubService { 20 | companion object { 21 | const val baseUrl = "https://api.github.com/" 22 | } 23 | 24 | @Throws 25 | @Headers( 26 | "Accept: application/vnd.github.v3+json", 27 | "Authorization: token ghp_abcdefgh", 28 | "Content-Type: application/json" 29 | ) 30 | @POST("repos/foso/experimental/issues") 31 | suspend fun createIssue( 32 | @Body body: Map<*, String>, 33 | @Header("Acci") headi: String? 34 | ): String 35 | 36 | @POST("repos/foso/experimental/issues") 37 | suspend fun createIssue2( 38 | @Body body: Issuedata, 39 | @Header("Acci") headi: String? 40 | ): Call> 41 | 42 | @Headers( 43 | "Accept: application/vnd.github.v3+json", 44 | "Authorization: token ghp_abcdefgh", 45 | "Content-Type: application/json" 46 | ) 47 | @GET("user/followers") 48 | fun getFollowers(): Flow> 49 | 50 | @Headers( 51 | "Accept: application/vnd.github.v3+json", 52 | "Authorization: token ghp_abcdefgh", 53 | "Content-Type: application/json" 54 | ) 55 | @GET("repos/{owner}/{repo}/commits") 56 | fun listCommits( 57 | @Path owner: String, 58 | @Path repo: String 59 | ): Flow> 60 | } 61 | --------------------------------------------------------------------------------