├── .fleet └── run.json ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── xcuserdata │ │ └── cooder.xcuserdatad │ │ └── xcschemes │ │ ├── [Fleet] [ios].xcscheme │ │ └── xcschememanagement.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── app-icon-1024.png │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── ktorfitx-annotation ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── cn │ └── vividcode │ └── multiplatform │ └── ktorfitx │ └── annotation │ ├── BearerAuth.kt │ ├── Body.kt │ ├── DELETE.kt │ ├── Form.kt │ ├── GET.kt │ ├── HEAD.kt │ ├── Header.kt │ ├── Headers.kt │ ├── OPTIONS.kt │ ├── PATCH.kt │ ├── POST.kt │ ├── PUT.kt │ ├── Path.kt │ └── Query.kt ├── ktorfitx-api ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── cn │ └── vividcode │ └── multiplatform │ └── ktorfitx │ ├── annotation │ ├── Api.kt │ ├── ExceptionListeners.kt │ ├── KtorfitDsl.kt │ ├── Mock.kt │ └── MockDsl.kt │ └── api │ ├── Ktorfit.kt │ ├── config │ ├── ApiScopeConfig.kt │ ├── KtorfitConfig.kt │ ├── LogConfig.kt │ └── MockClientConfig.kt │ ├── exception │ ├── AllExceptionListener.kt │ └── ExceptionListener.kt │ ├── expends │ └── HttpResponseExpends.kt │ ├── mock │ ├── MockClient.kt │ ├── MockLogging.kt │ ├── MockProvider.kt │ ├── MockRequestBuilder.kt │ └── MockStatus.kt │ ├── model │ └── ResultBody.kt │ └── scope │ ├── ApiScope.kt │ └── DefaultApiScope.kt ├── ktorfitx-ksp ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── cn │ │ └── vividcode │ │ └── multiplatform │ │ └── ktorfitx │ │ └── ksp │ │ ├── KtorfitSymbolProcessor.kt │ │ ├── KtorfitSymbolProcessorProvider.kt │ │ ├── check │ │ ├── CompileCheck.kt │ │ └── KtorfitxCompileCheckException.kt │ │ ├── constants │ │ ├── KtorQualifiers.kt │ │ └── KtorfitxQualifiers.kt │ │ ├── expends │ │ ├── ClassKindExpends.kt │ │ ├── KSPSymbolExpends.kt │ │ ├── KotlinPoetExpends.kt │ │ └── StringExpends.kt │ │ ├── kotlinpoet │ │ ├── ApiKotlinPoet.kt │ │ ├── ReturnTypes.kt │ │ └── block │ │ │ ├── ClientCodeBlock.kt │ │ │ ├── CodeBlockBuilder.kt │ │ │ ├── HttpClientCodeBlock.kt │ │ │ ├── MockClientCodeBlock.kt │ │ │ └── UseImports.kt │ │ ├── model │ │ ├── RequestMethod.kt │ │ ├── model │ │ │ ├── ApiModel.kt │ │ │ ├── BearerAuthModel.kt │ │ │ ├── BodyModel.kt │ │ │ ├── ExceptionListenerModel.kt │ │ │ ├── FormModel.kt │ │ │ ├── FunctionModel.kt │ │ │ ├── HeaderModel.kt │ │ │ ├── HeadersModel.kt │ │ │ ├── MockModel.kt │ │ │ ├── ParameterModel.kt │ │ │ ├── PathModel.kt │ │ │ ├── QueryModel.kt │ │ │ └── ValueParameterModel.kt │ │ └── structure │ │ │ ├── ApiStructure.kt │ │ │ ├── ClassStructure.kt │ │ │ ├── FunStructure.kt │ │ │ └── ReturnStructure.kt │ │ └── visitor │ │ ├── ApiVisitor.kt │ │ ├── VisitorResult.kt │ │ └── resolver │ │ ├── ApiModelResolver.kt │ │ ├── BearerAuthModelResolver.kt │ │ ├── BodyModelResolver.kt │ │ ├── ExceptionListenerResolver.kt │ │ ├── FormModelResolver.kt │ │ ├── HeaderModelResolver.kt │ │ ├── HeadersModelResolver.kt │ │ ├── MockModelResolver.kt │ │ ├── ModelResolvers.kt │ │ ├── ParameterModelResolver.kt │ │ ├── PathModelResolver.kt │ │ └── QueryModelResolver.kt │ └── resources │ └── META-INF │ └── services │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider ├── ktorfitx-sample ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── cn │ │ │ └── vividcode │ │ │ └── multiplatform │ │ │ └── ktorfitx │ │ │ └── sample │ │ │ ├── MainActivity.kt │ │ │ └── MainApplication.kt │ └── res │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ └── strings.xml │ ├── commonMain │ ├── composeResources │ │ └── drawable │ │ │ └── compose-multiplatform.xml │ └── kotlin │ │ └── cn │ │ └── vividcode │ │ └── multiplatform │ │ └── ktorfitx │ │ └── sample │ │ ├── App.kt │ │ └── http │ │ ├── TestKtorfit.kt │ │ ├── TestRequest2.kt │ │ ├── api │ │ ├── Test.kt │ │ ├── Test2Api.kt │ │ └── TestApi.kt │ │ ├── listener │ │ └── TestExceptionListener.kt │ │ └── mock │ │ ├── ResultBodyMockProvider.kt │ │ └── StringMockProvider.kt │ ├── desktopMain │ └── kotlin │ │ └── cn │ │ └── vividcode │ │ └── multiplatform │ │ └── ktorfitx │ │ └── sample │ │ └── main.kt │ ├── iosMain │ └── kotlin │ │ └── MainViewController.kt │ ├── jsMain │ ├── kotlin │ │ └── cn │ │ │ └── vividcode │ │ │ └── multiplatform │ │ │ └── ktorfitx │ │ │ └── sample │ │ │ └── main.kt │ └── resources │ │ ├── index.html │ │ └── styles.css │ └── wasmJsMain │ ├── kotlin │ └── cn │ │ └── vividcode │ │ └── multiplatform │ │ └── ktorfitx │ │ └── sample │ │ └── main.kt │ └── resources │ ├── index.html │ └── styles.css └── settings.gradle.kts /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "[android]", 5 | "type": "android-app", 6 | "workingDir": "$PROJECT_DIR$", 7 | "allowParallelRun": true, 8 | "module": "ktorfitx.ktorfitx-sample.main" 9 | }, 10 | { 11 | "name": "[ios]", 12 | "type": "xcode-app", 13 | "workingDir": "$PROJECT_DIR$", 14 | "allowParallelRun": true, 15 | "buildTarget": { 16 | "project": "iosApp", 17 | "target": "iosApp" 18 | }, 19 | "configuration": "Debug" 20 | }, 21 | { 22 | "name": "[desktop]", 23 | "type": "gradle", 24 | "workingDir": "$PROJECT_DIR$", 25 | "tasks": [ 26 | "desktopRun" 27 | ], 28 | "args": [ 29 | "-DmainClass=cn.vividcode.multiplatform.ktorfitx.sample.MainKt", 30 | "--quiet", 31 | "-p", 32 | "$PROJECT_DIR$/ktorfitx-sample" 33 | ], 34 | "initScripts": { 35 | "flmapper": "ext.mapPath = { path -> path }" 36 | } 37 | }, 38 | { 39 | "name": "[wasmJs]", 40 | "type": "gradle", 41 | "workingDir": "$PROJECT_DIR$", 42 | "tasks": [ 43 | "wasmJsBrowserDevelopmentRun" 44 | ], 45 | "args": [ 46 | "-p", 47 | "$PROJECT_DIR$/ktorfitx-sample" 48 | ], 49 | "initScripts": { 50 | "flmapper": "ext.mapPath = { path -> path }" 51 | } 52 | }, 53 | { 54 | "name": "[js]", 55 | "type": "gradle", 56 | "workingDir": "$PROJECT_DIR$", 57 | "tasks": [ 58 | "jsBrowserDevelopmentRun" 59 | ], 60 | "args": [ 61 | "-p", 62 | "$PROJECT_DIR$/ktorfitx-sample" 63 | ], 64 | "initScripts": { 65 | "flmapper": "ext.mapPath = { path -> path }" 66 | } 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store 43 | 44 | gradle.properties 45 | /file 46 | .idea 47 | /.run 48 | /.kotlin 49 | /local.properties 50 | /kotlin-js-store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 LI-JIA-WEI 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ktorfitx 3.1.3-2.4.0 2 | 3 | [![Maven](https://img.shields.io/badge/Maven-Central-download.svg)](https://central.sonatype.com/search?q=cn.vividcode.multiplatform:ktorfitx-api) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://vividcodex.github.io/ktorfitx-document/index_md.html) 5 | [![License](https://img.shields.io/badge/Apache-2.0-brightgreen.svg)](https://github.com/vividcodex/ktorfitx/blob/master/LICENSE-2.0) 6 | 7 | ## 更新时间 8 | 9 | ### 2025-05-16 10 | 11 | ## 文档 12 | 13 | [点击前往 Ktorfitx 官方文档](http://vividcodex.github.io/ktorfitx-document/index_md.html) 14 | 15 | ## 版本说明 16 | 17 | Kotlin `2.1.21` 18 | 19 | Ktor `3.1.3` 20 | 21 | KSP `2.1.21-2.0.1` 22 | 23 | ## 支持平台 24 | 25 | Android, IOS, Desktop (JVM), WasmJs, Js 26 | 27 | ## 依赖说明 28 | 29 | 请使用和 ktorfitx 相同版本的 ktor 版本,以保证兼容性 30 | 31 | ## 使用方法 32 | 33 | - 请在多平台模块中的 build.gradle.kts 配置一下内容,请按照实际情况编写 34 | 35 | ``` kotlin 36 | plugins { 37 | id("com.google.devtools.ksp") 38 | id("org.jetbrains.kotlin.plugin.serialization") 39 | } 40 | 41 | val ktorfitxVersion = "3.1.3-2.4.0" 42 | 43 | kotlin { 44 | sourceSets { 45 | commonMain.dependencies { 46 | implementation("cn.vividcode.multiplatform:ktorfitx-api:ktorfitxVersion") 47 | } 48 | commonMain { 49 | kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") 50 | } 51 | } 52 | } 53 | 54 | dependencies { 55 | kspCommonMainMetadata("cn.vividcode.multiplatform:ktorfitx-ksp:ktorfitxVersion") 56 | } 57 | 58 | tasks.withType>().all { 59 | "kspCommonMainKotlinMetadata".also { 60 | if (name != it) dependsOn(it) 61 | } 62 | } 63 | ``` 64 | 65 | ## 注解介绍 66 | 67 | ### `@Api` `接口` 标记在接口上 68 | 69 | ``` kotlin 70 | @Target(AnnotationTarget.CLASS) 71 | @Retention(AnnotationRetention.SOURCE) 72 | annotation class Api( 73 | 74 | // 接口前缀 75 | val url: String = "", 76 | 77 | // 接口作用域 78 | val apiScope: KClass = DefaultApiScope::class 79 | 80 | // 多个接口作用域 81 | val apiScopes: Array> = [], 82 | ) 83 | ``` 84 | 85 | ### `@BearerAuth` `方法` 授权 86 | 87 | ``` kotlin 88 | @Target(AnnotationTarget.FUNCTION) 89 | @Retention(AnnotationRetention.SOURCE) 90 | annotation class BearerAuth 91 | ``` 92 | 93 | ### `@GET` `方法` GET请求 94 | 95 | ``` kotlin 96 | @Target(AnnotationTarget.FUNCTION) 97 | @Retention(AnnotationRetention.SOURCE) 98 | annotation class GET( 99 | 100 | // 接口路径 101 | val url: String 102 | ) 103 | ``` 104 | 105 | ### `@POST` `方法` POST请求 106 | 107 | ``` kotlin 108 | @Target(AnnotationTarget.FUNCTION) 109 | @Retention(AnnotationRetention.SOURCE) 110 | annotation class POST( 111 | 112 | // 接口路径 113 | val url: String 114 | ) 115 | ``` 116 | 117 | ### `@PUT` `方法` PUT请求 118 | 119 | ``` kotlin 120 | @Target(AnnotationTarget.FUNCTION) 121 | @Retention(AnnotationRetention.SOURCE) 122 | annotation class PUT( 123 | 124 | // 接口路径 125 | val url: String 126 | ) 127 | ``` 128 | 129 | ### `@DELETE` `方法` DELETE请求 130 | 131 | ``` kotlin 132 | @Target(AnnotationTarget.FUNCTION) 133 | @Retention(AnnotationRetention.SOURCE) 134 | annotation class DELETE( 135 | 136 | // 接口路径 137 | val url: String 138 | ) 139 | ``` 140 | 141 | ### `@OPTIONS` `方法` OPTIONS请求 142 | 143 | ``` kotlin 144 | @Target(AnnotationTarget.FUNCTION) 145 | @Retention(AnnotationRetention.SOURCE) 146 | annotation class OPTIONS( 147 | 148 | // 接口路径 149 | val url: String 150 | ) 151 | ``` 152 | 153 | ### `@PATCH` `方法` PATCH请求 154 | 155 | ``` kotlin 156 | @Target(AnnotationTarget.FUNCTION) 157 | @Retention(AnnotationRetention.SOURCE) 158 | annotation class PATCH( 159 | 160 | // 接口路径 161 | val url: String 162 | ) 163 | ``` 164 | 165 | ### `@HEAD` `方法` HEAD请求 166 | 167 | ``` kotlin 168 | @Target(AnnotationTarget.FUNCTION) 169 | @Retention(AnnotationRetention.SOURCE) 170 | annotation class HEAD( 171 | 172 | // 接口路径 173 | val url: String 174 | ) 175 | ``` 176 | 177 | ### `@Headers` `方法` 请求头 178 | 179 | ``` kotlin 180 | @Target(AnnotationTarget.FUNCTION) 181 | @Retention(AnnotationRetention.SOURCE) 182 | annotation class Headers( 183 | 184 | // 请求头 名称:值 185 | val header: String, 186 | 187 | // 多个请求头 名称:值 188 | vararg val headers: String 189 | ) 190 | ``` 191 | 192 | ### `@ExceptionListeners` `方法` 异常监听器 193 | 194 | ``` kotlin 195 | @Target(AnnotationTarget.FUNCTION) 196 | @Retention(AnnotationRetention.SOURCE) 197 | annotation class ExceptionListeners( 198 | 199 | // 异常监听器 200 | val listener: KClass>, 201 | 202 | // 多个异常监听器 203 | vararg val listeners: KClass> 204 | ) 205 | ``` 206 | 207 | ### `@Mock` `参数` Mock 本地模拟请求 208 | 209 | ``` kotlin 210 | @Target(AnnotationTarget.FUNCTION) 211 | @Retention(AnnotationRetention.SOURCE) 212 | annotation class Mock( 213 | 214 | // Mock 提供者 215 | val provider: KClass>, 216 | 217 | // Mock 状态 218 | val status: MockStatus = MockStatus.SUCCESS, 219 | 220 | // Mock 延迟 221 | val delayRange: LongArray = [200L] 222 | ) 223 | ``` 224 | 225 | ### `@Body` `参数` Body请求体 226 | 227 | ``` kotlin 228 | @Target(AnnotationTarget.VALUE_PARAMETER) 229 | @Retention(AnnotationRetention.SOURCE) 230 | annotation class Body 231 | ``` 232 | 233 | ### `@Form` `参数` 表单 234 | 235 | ``` kotlin 236 | @Target(AnnotationTarget.VALUE_PARAMETER) 237 | @Retention(AnnotationRetention.SOURCE) 238 | annotation class Form( 239 | 240 | // 表单参数名称 默认:变量名 241 | val name: String = "" 242 | ) 243 | ``` 244 | 245 | ### `@Query` `参数` 查询参数 246 | 247 | ``` kotlin 248 | @Target(AnnotationTarget.VALUE_PARAMETER) 249 | @Retention(AnnotationRetention.SOURCE) 250 | annotation class Query( 251 | 252 | // 查询参数名称 默认:变量名 253 | val name: String = "" 254 | ) 255 | ``` 256 | 257 | ### `@Path` `参数` 路径参数 258 | 259 | ``` kotlin 260 | @Target(AnnotationTarget.VALUE_PARAMETER) 261 | @Retention(AnnotationRetention.SOURCE) 262 | annotation class Path( 263 | 264 | // 路径参数名称 默认:变量名 265 | val name: String = "" 266 | ) 267 | ``` 268 | 269 | ### `@Header` `参数` 请求头 270 | 271 | ``` kotlin 272 | @Target(AnnotationTarget.VALUE_PARAMETER) 273 | @Retention(AnnotationRetention.SOURCE) 274 | annotation class Header( 275 | 276 | // 请求头名称 默认:变量名 277 | val name: String = "" 278 | ) 279 | ``` 280 | 281 | ## 编译期错误检查 282 | 283 | 支持编译期错误检查,当您使用的方式不正确时,Ktorfitx 将会在编译期提供错误检查, 284 | 以帮助用户更快的定位错误 -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.org.jetbrains.kotlin.jvm) apply false 3 | alias(libs.plugins.kotlin.multiplatform) apply false 4 | alias(libs.plugins.compose.compiler) apply false 5 | alias(libs.plugins.kotlin.serialization) apply false 6 | alias(libs.plugins.android.application) apply false 7 | alias(libs.plugins.android.library) apply false 8 | alias(libs.plugins.jetbrains.compose) apply false 9 | alias(libs.plugins.maven.publish) apply false 10 | } -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | android-compileSdk = "35" 3 | android-minSdk = "24" 4 | android-targetSdk = "35" 5 | kotlin = "2.1.21" 6 | agp = "8.7.3" 7 | ktor = "3.1.3" 8 | ksp = "2.1.21-2.0.1" 9 | kotlinpoet-ksp = "2.2.0" 10 | maven-publish = "0.32.0" 11 | compose-plugin = "1.8.0" 12 | androidx-activity-compose = "1.10.1" 13 | 14 | [libraries] 15 | kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" } 16 | ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" } 17 | ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } 18 | ktor-client-cio = { group = "io.ktor", name = "ktor-client-cio", version.ref = "ktor" } 19 | ktor-client-serialization = { group = "io.ktor", name = "ktor-client-serialization", version.ref = "ktor" } 20 | ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" } 21 | ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } 22 | symbol-processing-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" } 23 | symbol-processing = { group = "com.google.devtools.ksp", name = "symbol-processing", version.ref = "ksp" } 24 | kotlinpoet-ksp = { group = "com.squareup", name = "kotlinpoet-ksp", version.ref = "kotlinpoet-ksp" } 25 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" } 26 | 27 | [plugins] 28 | android-application = { id = "com.android.application", version.ref = "agp" } 29 | android-library = { id = "com.android.library", version.ref = "agp" } 30 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 31 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 32 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 33 | org-jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 34 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 35 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } 36 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 09 15:38:29 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.12-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /iosApp/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=cn.vividcode.multiplatform.ktorfitx.sample.Ktorfitx 3 | APP_NAME=Ktorfitx -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/xcuserdata/cooder.xcuserdatad/xcschemes/[Fleet] [ios].xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/xcuserdata/cooder.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iosApp.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | [Fleet] [ios].xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 1 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import SampleApp 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | MainViewControllerKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | NSAppTransportSecurity 50 | 51 | NSAllowsArbitraryLoads 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /ktorfitx-annotation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.android.library) 9 | alias(libs.plugins.maven.publish) 10 | } 11 | 12 | val ktorfitxVersion = property("ktorfitx.version").toString() 13 | val ktorfitxAutomaticRelease = property("ktorfitx.automaticRelease").toString().toBoolean() 14 | 15 | group = "cn.vividcode.multiplatform.ktorfitx.annotation" 16 | version = ktorfitxVersion 17 | 18 | kotlin { 19 | jvmToolchain(21) 20 | 21 | androidTarget { 22 | compilerOptions { 23 | jvmTarget.set(JvmTarget.JVM_21) 24 | } 25 | } 26 | 27 | jvm("desktop") { 28 | compilerOptions { 29 | jvmTarget.set(JvmTarget.JVM_21) 30 | } 31 | } 32 | 33 | listOf( 34 | iosX64(), 35 | iosArm64(), 36 | iosSimulatorArm64() 37 | ).forEach { iosTarget -> 38 | iosTarget.binaries.framework { 39 | baseName = "KtorfitxAnnotation" 40 | isStatic = true 41 | } 42 | } 43 | 44 | js { 45 | outputModuleName = "ktorfitxAnnotation" 46 | browser { 47 | commonWebpackConfig { 48 | outputFileName = "ktorfitxAnnotation.js" 49 | } 50 | } 51 | binaries.executable() 52 | useEsModules() 53 | } 54 | 55 | 56 | @OptIn(ExperimentalWasmDsl::class) 57 | wasmJs { 58 | outputModuleName = "ktorfitxAnnotation" 59 | browser { 60 | val rootDirPath = project.rootDir.path 61 | val projectDirPath = project.projectDir.path 62 | commonWebpackConfig { 63 | outputFileName = "ktorfitxAnnotation.js" 64 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 65 | static = (static ?: mutableListOf()).apply { 66 | add(rootDirPath) 67 | add(projectDirPath) 68 | } 69 | } 70 | } 71 | } 72 | binaries.executable() 73 | } 74 | } 75 | 76 | android { 77 | namespace = "cn.vividcode.multiplatform.ktorfitx.annotation" 78 | compileSdk = libs.versions.android.compileSdk.get().toInt() 79 | 80 | sourceSets["main"].apply { 81 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 82 | res.srcDirs("src/androidMain/res") 83 | resources.srcDirs("src/commonMain/resources") 84 | } 85 | 86 | defaultConfig { 87 | minSdk = libs.versions.android.minSdk.get().toInt() 88 | } 89 | packaging { 90 | resources { 91 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 92 | merges += "/META-INF/DEPENDENCIES" 93 | } 94 | } 95 | buildTypes { 96 | getByName("release") { 97 | isMinifyEnabled = false 98 | } 99 | } 100 | compileOptions { 101 | sourceCompatibility = JavaVersion.VERSION_21 102 | targetCompatibility = JavaVersion.VERSION_21 103 | } 104 | } 105 | 106 | fun checkVersion() { 107 | val size = ktorfitxVersion.split("-").size 108 | check((ktorfitxAutomaticRelease && size == 2) || (!ktorfitxAutomaticRelease && size == 3)) { 109 | "ktorfitx 的 version 是 $ktorfitxVersion,但是 automaticRelease 是 $ktorfitxAutomaticRelease 的" 110 | } 111 | } 112 | 113 | mavenPublishing { 114 | checkVersion() 115 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, ktorfitxAutomaticRelease) 116 | signAllPublications() 117 | 118 | coordinates("cn.vividcode.multiplatform", "ktorfitx-annotation", ktorfitxVersion) 119 | 120 | pom { 121 | name.set("ktorfitx-api") 122 | description.set("Ktorfitx 基于 Ktor 的 RESTful API 框架") 123 | inceptionYear.set("2024") 124 | url.set("https://github.com/vividcodex/ktorfitx") 125 | licenses { 126 | license { 127 | name.set("The Apache License, Version 2.0") 128 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 129 | distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 130 | } 131 | } 132 | developers { 133 | developer { 134 | id.set("li-jia-wei") 135 | name.set("li-jia-wei") 136 | url.set("https://github.com/vividcodex/ktorfitx") 137 | } 138 | } 139 | 140 | scm { 141 | url.set("https://github.com/vividcodex/ktorfitx") 142 | connection.set("scm:git:git://github.com/vividcodex/ktorfitx.git") 143 | developerConnection.set("scm:git:ssh://git@github.com:vividcodex/ktorfitx.git") 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/BearerAuth.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/12 14:53 9 | * 10 | * 文件介绍:BearerAuth 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class BearerAuth -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Body.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 21:10 9 | * 10 | * 文件介绍:Body 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Body -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/DELETE.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/7 19:04 9 | * 10 | * 文件介绍:DELETE 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class DELETE( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Form.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 21:21 9 | * 10 | * 文件介绍:Form 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Form( 15 | val name: String = "" 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/GET.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 20:44 9 | * 10 | * 文件介绍:GET 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class GET( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/HEAD.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/14 22:01 9 | * 10 | * 文件介绍:HEAD 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class HEAD( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Header.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 21:26 9 | * 10 | * 文件介绍:Header 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Header( 15 | val name: String = "" 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Headers.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/12 16:40 9 | * 10 | * 文件介绍:Headers 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Headers( 15 | vararg val headers: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/OPTIONS.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/14 22:01 9 | * 10 | * 文件介绍:OPTIONS 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class OPTIONS( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/PATCH.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/14 22:17 9 | * 10 | * 文件介绍:PATCH 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class PATCH( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/POST.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 21:04 9 | * 10 | * 文件介绍:POST 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class POST( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/PUT.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/7 19:04 9 | * 10 | * 文件介绍:PUT 11 | */ 12 | @Target(AnnotationTarget.FUNCTION) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class PUT( 15 | val url: String 16 | ) -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Path.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/5/14 15:24 9 | * 10 | * 文件介绍:Path 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Path(val name: String = "") -------------------------------------------------------------------------------- /ktorfitx-annotation/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Query.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/3/23 21:09 9 | * 10 | * 文件介绍:Query 11 | */ 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | @Retention(AnnotationRetention.SOURCE) 14 | annotation class Query( 15 | val name: String = "" 16 | ) -------------------------------------------------------------------------------- /ktorfitx-api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.jetbrains.compose) 10 | alias(libs.plugins.kotlin.serialization) 11 | alias(libs.plugins.android.library) 12 | alias(libs.plugins.maven.publish) 13 | } 14 | 15 | val ktorfitxVersion = property("ktorfitx.version").toString() 16 | val ktorfitxAutomaticRelease = property("ktorfitx.automaticRelease").toString().toBoolean() 17 | 18 | group = "cn.vividcode.multiplatform.ktorfitx.api" 19 | version = ktorfitxVersion 20 | 21 | kotlin { 22 | jvmToolchain(21) 23 | 24 | androidTarget { 25 | compilerOptions { 26 | jvmTarget.set(JvmTarget.JVM_21) 27 | } 28 | } 29 | 30 | jvm("desktop") { 31 | compilerOptions { 32 | jvmTarget.set(JvmTarget.JVM_21) 33 | } 34 | } 35 | 36 | listOf( 37 | iosX64(), 38 | iosArm64(), 39 | iosSimulatorArm64() 40 | ).forEach { iosTarget -> 41 | iosTarget.binaries.framework { 42 | baseName = "KtorfitxApi" 43 | isStatic = true 44 | } 45 | } 46 | 47 | js { 48 | outputModuleName = "ktorfitxApi" 49 | browser { 50 | commonWebpackConfig { 51 | outputFileName = "ktorfitxApi.js" 52 | } 53 | } 54 | binaries.executable() 55 | useEsModules() 56 | } 57 | 58 | @OptIn(ExperimentalWasmDsl::class) 59 | wasmJs { 60 | outputModuleName = "ktorfitxApi" 61 | browser { 62 | val rootDirPath = project.rootDir.path 63 | val projectDirPath = project.projectDir.path 64 | commonWebpackConfig { 65 | outputFileName = "ktorfitxApi.js" 66 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 67 | static = (static ?: mutableListOf()).apply { 68 | add(rootDirPath) 69 | add(projectDirPath) 70 | } 71 | } 72 | } 73 | } 74 | binaries.executable() 75 | } 76 | 77 | sourceSets { 78 | commonMain { 79 | dependencies { 80 | implementation(compose.runtime) 81 | implementation(libs.kotlin.reflect) 82 | api(projects.ktorfitxAnnotation) 83 | implementation(libs.ktor.client.core) 84 | implementation(libs.ktor.client.serialization) 85 | implementation(libs.ktor.client.logging) 86 | } 87 | } 88 | } 89 | } 90 | 91 | android { 92 | namespace = "cn.vividcode.multiplatform.ktorfitx.api" 93 | compileSdk = libs.versions.android.compileSdk.get().toInt() 94 | 95 | sourceSets["main"].apply { 96 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 97 | res.srcDirs("src/androidMain/res") 98 | resources.srcDirs("src/commonMain/resources") 99 | } 100 | 101 | defaultConfig { 102 | minSdk = libs.versions.android.minSdk.get().toInt() 103 | } 104 | packaging { 105 | resources { 106 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 107 | merges += "/META-INF/DEPENDENCIES" 108 | } 109 | } 110 | buildTypes { 111 | getByName("release") { 112 | isMinifyEnabled = false 113 | } 114 | } 115 | compileOptions { 116 | sourceCompatibility = JavaVersion.VERSION_21 117 | targetCompatibility = JavaVersion.VERSION_21 118 | } 119 | } 120 | 121 | fun checkVersion() { 122 | val size = ktorfitxVersion.split("-").size 123 | check((ktorfitxAutomaticRelease && size == 2) || (!ktorfitxAutomaticRelease && size == 3)) { 124 | "ktorfitx 的 version 是 $ktorfitxVersion,但是 automaticRelease 是 $ktorfitxAutomaticRelease 的" 125 | } 126 | } 127 | 128 | mavenPublishing { 129 | checkVersion() 130 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, ktorfitxAutomaticRelease) 131 | signAllPublications() 132 | 133 | coordinates("cn.vividcode.multiplatform", "ktorfitx-api", ktorfitxVersion) 134 | 135 | pom { 136 | name.set("ktorfitx-api") 137 | description.set("Ktorfitx 基于 Ktor 的 RESTful API 框架") 138 | inceptionYear.set("2024") 139 | url.set("https://github.com/vividcodex/ktorfitx") 140 | licenses { 141 | license { 142 | name.set("The Apache License, Version 2.0") 143 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 144 | distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 145 | } 146 | } 147 | developers { 148 | developer { 149 | id.set("li-jia-wei") 150 | name.set("li-jia-wei") 151 | url.set("https://github.com/vividcodex/ktorfitx") 152 | } 153 | } 154 | 155 | scm { 156 | url.set("https://github.com/vividcodex/ktorfitx") 157 | connection.set("scm:git:git://github.com/vividcodex/ktorfitx.git") 158 | developerConnection.set("scm:git:ssh://git@github.com:vividcodex/ktorfitx.git") 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Api.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.scope.ApiScope 4 | import cn.vividcode.multiplatform.ktorfitx.api.scope.DefaultApiScope 5 | import kotlin.reflect.KClass 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/3/23 21:06 13 | * 14 | * 文件介绍:Api 15 | */ 16 | @Target(AnnotationTarget.CLASS) 17 | @Retention(AnnotationRetention.SOURCE) 18 | annotation class Api( 19 | val url: String = "", 20 | val apiScope: KClass = DefaultApiScope::class, 21 | val apiScopes: Array> = [], 22 | ) -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/ExceptionListeners.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.exception.ExceptionListener 4 | import kotlin.reflect.KClass 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/8/13 14:38 12 | * 13 | * 文件介绍:ExceptionListeners 14 | */ 15 | @Target(AnnotationTarget.FUNCTION) 16 | @Retention(AnnotationRetention.SOURCE) 17 | annotation class ExceptionListeners( 18 | val listener: KClass>, 19 | vararg val listeners: KClass> 20 | ) -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/KtorfitDsl.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/5 12:52 9 | * 10 | * 文件介绍:KtorfitDsl 11 | */ 12 | @DslMarker 13 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 14 | internal annotation class KtorfitDsl -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/Mock.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockProvider 4 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus 5 | import kotlin.reflect.KClass 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/8/12 03:40 13 | * 14 | * 文件介绍:Mock 15 | */ 16 | @Target(AnnotationTarget.FUNCTION) 17 | @Retention(AnnotationRetention.SOURCE) 18 | annotation class Mock( 19 | val provider: KClass>, 20 | val status: MockStatus = MockStatus.SUCCESS, 21 | val delayRange: LongArray = [200L] 22 | ) -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/annotation/MockDsl.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.annotation 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/11 04:23 9 | * 10 | * 文件介绍:MockDsl 11 | */ 12 | @DslMarker 13 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 14 | internal annotation class MockDsl -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/Ktorfit.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.config.KtorfitConfig 4 | import cn.vividcode.multiplatform.ktorfitx.api.scope.ApiScope 5 | import cn.vividcode.multiplatform.ktorfitx.api.scope.DefaultApiScope 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/5/9 23:52 13 | * 14 | * 文件介绍:Ktorfit 15 | */ 16 | @Suppress("MemberVisibilityCanBePrivate", "unused") 17 | class Ktorfit internal constructor( 18 | val config: KtorfitConfig, 19 | private val apiScope: AS, 20 | ) 21 | 22 | /** 23 | * ktorfit 24 | */ 25 | fun ktorfit( 26 | config: KtorfitConfig.() -> Unit, 27 | ): Ktorfit = KtorfitConfig() 28 | .apply(config) 29 | .build(DefaultApiScope) 30 | 31 | /** 32 | * ktorfit 33 | */ 34 | fun ktorfit( 35 | apiScope: AS, 36 | config: KtorfitConfig.() -> Unit, 37 | ): Ktorfit = KtorfitConfig() 38 | .apply(config) 39 | .build(apiScope) -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/config/ApiScopeConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.config 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.KtorfitDsl 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/8/10 18:53 11 | * 12 | * 文件介绍:ApiScopeConfig 13 | */ 14 | @KtorfitDsl 15 | class ApiScopeConfig internal constructor() { 16 | 17 | var printName: Boolean = false 18 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/config/KtorfitConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.config 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.KtorfitDsl 4 | import cn.vividcode.multiplatform.ktorfitx.api.Ktorfit 5 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockClient 6 | import cn.vividcode.multiplatform.ktorfitx.api.scope.ApiScope 7 | import io.ktor.client.* 8 | import io.ktor.client.engine.* 9 | import io.ktor.client.plugins.* 10 | 11 | /** 12 | * 项目名称:ktorfitx 13 | * 14 | * 作者昵称:li-jia-wei 15 | * 16 | * 创建日期:2024/8/10 20:40 17 | * 18 | * 文件介绍:KtorfitConfig 19 | */ 20 | @KtorfitDsl 21 | class KtorfitConfig internal constructor() { 22 | 23 | var baseUrl: String? = null 24 | 25 | var token: (() -> String?)? = null 26 | private set 27 | 28 | var httpClient: HttpClient? = null 29 | private set 30 | 31 | var mockClient: MockClient? = null 32 | private set 33 | 34 | private var httpClientBlock: HttpClientBlock? = null 35 | 36 | fun httpClient( 37 | engineFactory: HttpClientEngineFactory, 38 | block: HttpClientConfig.() -> Unit = {} 39 | ) { 40 | @Suppress("UNCHECKED_CAST") 41 | this.httpClientBlock = HttpClientBlock(engineFactory, block as (HttpClientConfig<*>.() -> Unit)) 42 | } 43 | 44 | fun mockClient(builder: MockClientConfig.() -> Unit) { 45 | val config = MockClientConfig().apply(builder).build() 46 | this.mockClient = MockClient(config.log!!, config.json!!) 47 | } 48 | 49 | internal var apiScope: ApiScopeConfig? = null 50 | private set 51 | 52 | fun token(token: () -> String?) { 53 | this.token = token 54 | } 55 | 56 | fun apiScope(config: ApiScopeConfig.() -> Unit) { 57 | this.apiScope = ApiScopeConfig().apply(config) 58 | } 59 | 60 | fun build(apiScope: AS): Ktorfit { 61 | this.token = this.token ?: { null } 62 | this.apiScope = this.apiScope ?: ApiScopeConfig() 63 | this.httpClient = if (httpClientBlock == null) HttpClient() else with(httpClientBlock!!) { 64 | HttpClient(engine) { 65 | defaultRequest { 66 | if (baseUrl != null) { 67 | url(baseUrl!!) 68 | } 69 | } 70 | block() 71 | } 72 | } 73 | this.mockClient = this.mockClient ?: MockClient() 74 | return Ktorfit(this, apiScope) 75 | } 76 | 77 | private class HttpClientBlock( 78 | val engine: HttpClientEngineFactory<*>, 79 | val block: HttpClientConfig<*>.() -> Unit 80 | ) 81 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/config/LogConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.config 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.KtorfitDsl 4 | import io.ktor.client.plugins.logging.* 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/8/10 20:38 12 | * 13 | * 文件介绍:LogConfig 14 | */ 15 | @KtorfitDsl 16 | class LogConfig internal constructor() { 17 | 18 | var level: LogLevel = LogLevel.HEADERS 19 | 20 | var logger: (String) -> Unit = ::println 21 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/config/MockClientConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.config 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.KtorfitDsl 4 | import kotlinx.serialization.json.Json 5 | import kotlinx.serialization.json.JsonBuilder 6 | 7 | @KtorfitDsl 8 | class MockClientConfig internal constructor() { 9 | 10 | var json: Json? = null 11 | private set 12 | 13 | fun json(builder: JsonBuilder.() -> Unit) { 14 | this.json = Json(builderAction = builder) 15 | } 16 | 17 | var log: LogConfig? = null 18 | private set 19 | 20 | fun log(builder: LogConfig.() -> Unit) { 21 | this.log = LogConfig().apply(builder) 22 | } 23 | 24 | fun build(): MockClientConfig { 25 | if (json == null) { 26 | json = Json 27 | } 28 | if (log == null) { 29 | log = LogConfig() 30 | } 31 | return this 32 | } 33 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/exception/AllExceptionListener.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.exception 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/18 20:39 9 | * 10 | * 文件介绍:AllExceptionListener 11 | */ 12 | interface AllExceptionListener : ExceptionListener -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/exception/ExceptionListener.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.exception 2 | 3 | import kotlin.reflect.KFunction 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/8/13 14:40 11 | * 12 | * 文件介绍:ExceptionListener 13 | */ 14 | interface ExceptionListener { 15 | 16 | fun KFunction<*>.onExceptionListener(e: E): R 17 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/expends/HttpResponseExpends.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.expends 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.model.ResultBody 4 | import io.ktor.client.call.* 5 | import io.ktor.client.statement.* 6 | import io.ktor.http.* 7 | 8 | /** 9 | * 获取 ResultBody,如果失败返回 ResultBody.failure() 10 | */ 11 | suspend inline fun HttpResponse.safeResultBody(): ResultBody { 12 | return if (this.status.isSuccess()) { 13 | this.body() 14 | } else { 15 | ResultBody.failure(this.status.value, this.status.description) 16 | } 17 | } 18 | 19 | /** 20 | * 获取 ResultBody,如果失败返回 null 21 | */ 22 | suspend inline fun HttpResponse.safeResultBodyOrNull(): ResultBody? { 23 | return if (this.status.isSuccess()) { 24 | this.body() 25 | } else null 26 | } 27 | 28 | /** 29 | * 获取 ByteArray,如果失败返回 EmptyByteArray 30 | */ 31 | suspend fun HttpResponse.safeByteArray(): ByteArray { 32 | return if (this.status.isSuccess()) { 33 | this.readRawBytes() 34 | } else ByteArray(0) 35 | } 36 | 37 | /** 38 | * 获取 ByteArray,如果失败返回 null 39 | */ 40 | suspend fun HttpResponse.safeByteArrayOrNull(): ByteArray? { 41 | return if (this.status.isSuccess()) { 42 | this.readRawBytes() 43 | } else null 44 | } 45 | 46 | /** 47 | * 获取 String,如果失败返回 "" 48 | */ 49 | suspend fun HttpResponse.safeText(): String { 50 | return if (this.status.isSuccess()) { 51 | this.bodyAsText() 52 | } else "" 53 | } 54 | 55 | /** 56 | * 获取 String,如果失败返回 null 57 | */ 58 | suspend fun HttpResponse.safeTextOrNull(): String? { 59 | return if (this.status.isSuccess()) { 60 | this.bodyAsText() 61 | } else null 62 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/mock/MockClient.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.mock 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.MockDsl 4 | import cn.vividcode.multiplatform.ktorfitx.api.config.LogConfig 5 | import io.ktor.http.* 6 | import kotlinx.coroutines.delay 7 | import kotlinx.serialization.json.Json 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/8/11 03:42 15 | * 16 | * 文件介绍:MockClient 17 | */ 18 | @MockDsl 19 | class MockClient internal constructor( 20 | val log: LogConfig = LogConfig(), 21 | val json: Json = Json 22 | ) { 23 | 24 | suspend inline fun get( 25 | urlString: String, 26 | mockProvider: MockProvider, 27 | status: MockStatus, 28 | delayRange: LongRange, 29 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 30 | ): Mock = request(HttpMethod.Get, urlString, mockProvider, status, delayRange, builder) 31 | 32 | suspend inline fun post( 33 | urlString: String, 34 | mockProvider: MockProvider, 35 | status: MockStatus, 36 | delayRange: LongRange, 37 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 38 | ): Mock = request(HttpMethod.Post, urlString, mockProvider, status, delayRange, builder) 39 | 40 | suspend inline fun put( 41 | urlString: String, 42 | mockProvider: MockProvider, 43 | status: MockStatus, 44 | delayRange: LongRange, 45 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 46 | ): Mock = request(HttpMethod.Put, urlString, mockProvider, status, delayRange, builder) 47 | 48 | suspend inline fun delete( 49 | urlString: String, 50 | mockProvider: MockProvider, 51 | status: MockStatus, 52 | delayRange: LongRange, 53 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 54 | ): Mock = request(HttpMethod.Delete, urlString, mockProvider, status, delayRange, builder) 55 | 56 | suspend inline fun options( 57 | urlString: String, 58 | mockProvider: MockProvider, 59 | status: MockStatus, 60 | delayRange: LongRange, 61 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 62 | ): Mock = request(HttpMethod.Options, urlString, mockProvider, status, delayRange, builder) 63 | 64 | suspend inline fun head( 65 | urlString: String, 66 | mockProvider: MockProvider, 67 | status: MockStatus, 68 | delayRange: LongRange, 69 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 70 | ): Mock = request(HttpMethod.Head, urlString, mockProvider, status, delayRange, builder) 71 | 72 | suspend inline fun patch( 73 | urlString: String, 74 | mockProvider: MockProvider, 75 | status: MockStatus, 76 | delayRange: LongRange, 77 | noinline builder: (MockRequestBuilder.() -> Unit)? = null 78 | ): Mock = request(HttpMethod.Patch, urlString, mockProvider, status, delayRange, builder) 79 | 80 | suspend inline fun request( 81 | method: HttpMethod, 82 | urlString: String, 83 | mockProvider: MockProvider, 84 | status: MockStatus, 85 | delayRange: LongRange, 86 | noinline builder: (MockRequestBuilder.() -> Unit)? 87 | ): Mock { 88 | val mockRequest = MockRequestBuilder(this.json).let { if (builder != null) it.apply(builder) else it } 89 | val mockLogging = MockLogging(this.log, mockRequest, urlString) 90 | mockLogging.request(method) 91 | val delay = delayRange.random() 92 | delay(delay) 93 | val mock = mockProvider.provide(status) 94 | mockLogging.response(mock, json, delay) 95 | return mock 96 | } 97 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/mock/MockLogging.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.mock 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.config.LogConfig 4 | import io.ktor.client.plugins.logging.LogLevel.* 5 | import io.ktor.http.* 6 | import kotlinx.serialization.encodeToString 7 | import kotlinx.serialization.json.Json 8 | import kotlin.time.Duration.Companion.milliseconds 9 | 10 | /** 11 | * 项目名称:ktorfitx 12 | * 13 | * 作者昵称:li-jia-wei 14 | * 15 | * 创建日期:2024/8/11 04:34 16 | * 17 | * 文件介绍:MockLogging 18 | */ 19 | class MockLogging( 20 | val log: LogConfig, 21 | private val mockRequest: MockRequestBuilder, 22 | urlString: String, 23 | ) { 24 | 25 | val urlString: String by lazy { 26 | formatUrl(urlString, mockRequest.paths, mockRequest.queries) 27 | } 28 | 29 | fun request(method: HttpMethod) { 30 | if (log.level != NONE) { 31 | buildString { 32 | if (log.level.info) { 33 | appendLine("REQUEST: $urlString") 34 | appendLine("METHOD: ${method.value}") 35 | } 36 | if (log.level.headers && mockRequest.headers.isNotEmpty()) { 37 | appendLine("HEADERS: COUNT=${mockRequest.headers.size}") 38 | mockRequest.headers.forEach { (name, value) -> 39 | appendLine("-> $name: $value") 40 | } 41 | } 42 | if (log.level.body) { 43 | if (mockRequest.forms.isNotEmpty()) { 44 | appendLine("FORMS: COUNT=${mockRequest.forms.size}") 45 | mockRequest.forms.forEach { (name, value) -> 46 | appendLine("-> $name: $value") 47 | } 48 | } 49 | if (mockRequest.body != null) { 50 | appendLine("BODY START: LENGTH=${mockRequest.body!!.length}") 51 | appendLine(mockRequest.body!!) 52 | appendLine("BODY END") 53 | } 54 | } 55 | }.let(log.logger) 56 | } 57 | } 58 | 59 | inline fun response( 60 | mock: T, 61 | json: Json, 62 | delay: Long 63 | ) { 64 | if (log.level != NONE) { 65 | buildString { 66 | if (log.level.info) { 67 | appendLine("RESPONSE: $urlString") 68 | appendLine("DELAY TIME: ${delay.milliseconds}") 69 | } 70 | if (log.level.body) { 71 | val stringJson = json.encodeToString(mock) 72 | val length = stringJson.filterNot { it == ' ' || it == '\n' }.length 73 | appendLine("BODY START: LENGTH=$length") 74 | appendLine(stringJson) 75 | appendLine("BODY END") 76 | } 77 | }.also(log.logger) 78 | } 79 | } 80 | 81 | private fun formatUrl( 82 | urlString: String, 83 | paths: Map, 84 | queries: Map 85 | ): String = buildString { 86 | val url = paths.entries.fold(urlString) { acc, path -> 87 | acc.replace("{${path.key}}", path.value.toString()) 88 | } 89 | append(url) 90 | queries.entries.forEachIndexed { index, query -> 91 | append(if (index == 0) '?' else '&') 92 | append(query.key) 93 | append('=') 94 | append(query.value) 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/mock/MockProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.mock 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/12 03:42 9 | * 10 | * 文件介绍:MockProvider 11 | */ 12 | interface MockProvider { 13 | 14 | fun provide(status: MockStatus): Mock 15 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/mock/MockRequestBuilder.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.mock 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.MockDsl 4 | import io.ktor.http.* 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | 8 | /** 9 | * 项目名称:ktorfitx 10 | * 11 | * 作者昵称:li-jia-wei 12 | * 13 | * 创建日期:2024/8/11 03:52 14 | * 15 | * 文件介绍:MockRequestBuilder 16 | */ 17 | @MockDsl 18 | class MockRequestBuilder( 19 | val json: Json 20 | ) { 21 | 22 | val headers = mutableMapOf() 23 | 24 | val queries = mutableMapOf() 25 | 26 | val forms = mutableMapOf() 27 | 28 | val paths = mutableMapOf() 29 | 30 | var body: String? = null 31 | 32 | fun bearerAuth(token: String) { 33 | this.headers[HttpHeaders.Authorization] = "Bearer $token" 34 | } 35 | 36 | fun headers(block: MutableMap.() -> Unit) { 37 | this.headers += mutableMapOf().apply(block) 38 | } 39 | 40 | fun queries(block: MutableMap.() -> Unit) { 41 | this.queries += mutableMapOf().apply(block) 42 | } 43 | 44 | fun forms(block: MutableMap.() -> Unit) { 45 | this.forms += mutableMapOf().apply(block) 46 | } 47 | 48 | fun paths(block: MutableMap.() -> Unit) { 49 | this.paths += mutableMapOf().apply(block) 50 | } 51 | 52 | inline fun body(body: T) { 53 | this.body = json.encodeToString(body) 54 | } 55 | 56 | fun MutableMap.append(name: String, value: Any) { 57 | this[name] = value 58 | } 59 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/mock/MockStatus.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.mock 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/11 03:37 9 | * 10 | * 文件介绍:MockStatus 11 | */ 12 | enum class MockStatus { 13 | 14 | SUCCESS, 15 | 16 | FAILURE, 17 | 18 | EXCEPTION 19 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/model/ResultBody.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/5/9 23:52 11 | * 12 | * 文件介绍:ResultBody 13 | */ 14 | @Serializable 15 | data class ResultBody( 16 | val code: Int, 17 | val msg: String, 18 | val data: T? = null 19 | ) { 20 | 21 | companion object { 22 | 23 | /** 24 | * 成功 25 | */ 26 | fun success(data: T?, msg: String): ResultBody { 27 | return ResultBody(0, msg, data) 28 | } 29 | 30 | /** 31 | * 失败 32 | */ 33 | fun failure(code: Int, msg: String): ResultBody { 34 | return ResultBody(code, msg) 35 | } 36 | 37 | /** 38 | * 异常 39 | */ 40 | fun exception(e: Exception): ResultBody { 41 | return ResultBody(-1, e.message ?: e.toString()) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/scope/ApiScope.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.scope 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/4 23:40 9 | * 10 | * 文件介绍:Api 作用域 11 | */ 12 | interface ApiScope { 13 | 14 | /** 15 | * Api 作用域名称 16 | */ 17 | val name: String 18 | get() = this::class.simpleName!! 19 | } -------------------------------------------------------------------------------- /ktorfitx-api/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/api/scope/DefaultApiScope.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.api.scope 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/18 22:39 9 | * 10 | * 文件介绍:默认的 ApiScope 11 | */ 12 | object DefaultApiScope : ApiScope { 13 | 14 | override val name: String = "默认的接口作用域" 15 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.jvm) 6 | alias(libs.plugins.maven.publish) 7 | } 8 | 9 | val ktorfitxVersion = property("ktorfitx.version").toString() 10 | val ktorfitxAutomaticRelease = property("ktorfitx.automaticRelease").toString().toBoolean() 11 | 12 | group = "cn.vividcode.multiplatform.ktorfitx.ksp" 13 | version = ktorfitxVersion 14 | 15 | java { 16 | sourceCompatibility = JavaVersion.VERSION_21 17 | } 18 | 19 | kotlin { 20 | jvmToolchain(21) 21 | 22 | compilerOptions { 23 | jvmTarget.set(JvmTarget.JVM_21) 24 | } 25 | } 26 | 27 | sourceSets { 28 | main { 29 | kotlin.srcDirs("build/generated/ksp/commonMain/kotlin") 30 | java.srcDirs("build/generated/ksp/commonMain/kotlin") 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation(projects.ktorfitxAnnotation) 36 | implementation(libs.kotlin.reflect) 37 | implementation(libs.symbol.processing.api) 38 | implementation(libs.symbol.processing) 39 | implementation(libs.kotlinpoet.ksp) 40 | } 41 | 42 | fun checkVersion() { 43 | val size = ktorfitxVersion.split("-").size 44 | check((ktorfitxAutomaticRelease && size == 2) || (!ktorfitxAutomaticRelease && size == 3)) { 45 | "ktorfitx 的 version 是 $ktorfitxVersion,但是 automaticRelease 是 $ktorfitxAutomaticRelease 的" 46 | } 47 | } 48 | 49 | mavenPublishing { 50 | checkVersion() 51 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, ktorfitxAutomaticRelease) 52 | signAllPublications() 53 | 54 | coordinates("cn.vividcode.multiplatform", "ktorfitx-ksp", ktorfitxVersion) 55 | 56 | pom { 57 | name.set("ktorfitx-ksp") 58 | description.set("Ktorfitx 基于 Ktor 的 RESTful API 框架") 59 | inceptionYear.set("2024") 60 | url.set("https://github.com/vividcodex/ktorfitx") 61 | licenses { 62 | license { 63 | name.set("The Apache License, Version 2.0") 64 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 65 | distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 66 | } 67 | } 68 | developers { 69 | developer { 70 | id.set("li-jia-wei") 71 | name.set("li-jia-wei") 72 | url.set("https://github.com/vividcodex/ktorfitx") 73 | } 74 | } 75 | 76 | scm { 77 | url.set("https://gitlab.com/vividcodex/ktorfitx") 78 | connection.set("scm:git:git://github.com/vividcodex/ktorfitx.git") 79 | developerConnection.set("scm:git:ssh://git@github.com:vividcodex/ktorfitx.git") 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/KtorfitSymbolProcessor.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.code 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.ApiKotlinPoet 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block.UseImports 8 | import cn.vividcode.multiplatform.ktorfitx.ksp.visitor.ApiVisitor 9 | import com.google.devtools.ksp.getClassDeclarationByName 10 | import com.google.devtools.ksp.processing.CodeGenerator 11 | import com.google.devtools.ksp.processing.Dependencies 12 | import com.google.devtools.ksp.processing.Resolver 13 | import com.google.devtools.ksp.processing.SymbolProcessor 14 | import com.google.devtools.ksp.symbol.ClassKind 15 | import com.google.devtools.ksp.symbol.KSAnnotated 16 | import com.google.devtools.ksp.symbol.KSClassDeclaration 17 | import com.google.devtools.ksp.symbol.Modifier 18 | import com.google.devtools.ksp.validate 19 | 20 | /** 21 | * 项目名称:ktorfitx 22 | * 23 | * 作者昵称:li-jia-wei 24 | * 25 | * 创建日期:2024/3/23 22:13 26 | * 27 | * 文件介绍:KtorfitSymbolProcessor 28 | */ 29 | internal class KtorfitSymbolProcessor( 30 | private val codeGenerator: CodeGenerator, 31 | ) : SymbolProcessor { 32 | 33 | private val apiKotlinPoet by lazy { ApiKotlinPoet() } 34 | private val processedSymbols = mutableSetOf() 35 | 36 | override fun process(resolver: Resolver): List { 37 | val annotatedList = resolver.getSymbolsWithAnnotation(KtorfitxQualifiers.API) 38 | annotatedList.forEach { symbol -> 39 | if (!symbol.validate()) return@forEach 40 | val classDeclaration = symbol as? KSClassDeclaration ?: return@forEach 41 | 42 | val classKind = classDeclaration.classKind 43 | classDeclaration.compileCheck(classKind == ClassKind.INTERFACE) { 44 | val className = classDeclaration.simpleName.asString() 45 | "$className 必须是 interface 类型的,而你使用了 ${classKind.code}" 46 | } 47 | classDeclaration.compileCheck(Modifier.SEALED !in classDeclaration.modifiers) { 48 | val className = classDeclaration.simpleName.asString() 49 | "$className 接口在当前版本中不支持 sealed interface,请使用 interface" 50 | } 51 | 52 | if (classDeclaration.isRepeatProcessing) return@forEach 53 | resolver.processing(classDeclaration) 54 | } 55 | return emptyList() 56 | } 57 | 58 | /** 59 | * 不允许处理 60 | */ 61 | private val KSClassDeclaration.isRepeatProcessing: Boolean 62 | get() { 63 | val qualifiedName = this.qualifiedName?.asString() ?: return true 64 | return !processedSymbols.add(qualifiedName) 65 | } 66 | 67 | /** 68 | * 开始处理 69 | */ 70 | private fun Resolver.processing(classDeclaration: KSClassDeclaration) { 71 | val apiVisitor = ApiVisitor(this) 72 | val visitorResult = classDeclaration.accept(apiVisitor, Unit) ?: return 73 | val fileSpec = apiKotlinPoet.getFileSpec(visitorResult.classStructure) 74 | val className = visitorResult.classStructure.className 75 | codeGenerator.createNewFile( 76 | dependencies = getDependencies(classDeclaration), 77 | packageName = className.packageName, 78 | fileName = className.simpleName 79 | ).bufferedWriter().use { 80 | fileSpec.writeTo(it) 81 | } 82 | } 83 | 84 | /** 85 | * 获取源文件 86 | */ 87 | private fun Resolver.getDependencies(classDeclaration: KSClassDeclaration): Dependencies { 88 | val importMap = UseImports.get().filterNot { 89 | it.key.startsWith("io.ktor") || 90 | it.key.startsWith(KtorfitxQualifiers.PACKAGE_API) 91 | } 92 | UseImports.clear() 93 | val ksFiles = importMap.flatMap { (packageName, simpleNames) -> 94 | simpleNames.map { 95 | val ksFile = this.getClassDeclarationByName("$packageName.$it")?.containingFile 96 | if (ksFile == null) { 97 | kspLogger?.warn("$packageName.$it 未能获取它的源文件") 98 | } 99 | ksFile 100 | } 101 | } + classDeclaration.containingFile 102 | val ksFileArray = ksFiles.filterNotNull() 103 | .groupBy { it.filePath } 104 | .map { it.value.first() } 105 | .toTypedArray() 106 | return Dependencies(false, *ksFileArray) 107 | } 108 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/KtorfitSymbolProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp 2 | 3 | import com.google.devtools.ksp.processing.KSPLogger 4 | import com.google.devtools.ksp.processing.SymbolProcessor 5 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | 8 | /** 9 | * 项目名称:ktorfitx 10 | * 11 | * 作者昵称:li-jia-wei 12 | * 13 | * 创建日期:2024/3/23 22:14 14 | * 15 | * 文件介绍:KtorfitSymbolProcessorProvider 16 | */ 17 | internal class KtorfitSymbolProcessorProvider : SymbolProcessorProvider { 18 | 19 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 20 | kspLoggers.set(environment.logger) 21 | return KtorfitSymbolProcessor(environment.codeGenerator) 22 | } 23 | } 24 | 25 | private val kspLoggers = ThreadLocal() 26 | 27 | val kspLogger: KSPLogger? 28 | get() = kspLoggers.get() -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/check/CompileCheck.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.check 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.kspLogger 4 | import com.google.devtools.ksp.symbol.KSNode 5 | import kotlin.contracts.ExperimentalContracts 6 | import kotlin.contracts.contract 7 | 8 | /** 9 | * 编译器检查 10 | */ 11 | @OptIn(ExperimentalContracts::class) 12 | internal inline fun T.compileCheck( 13 | value: Boolean, 14 | lazyErrorMessage: () -> String, 15 | ) { 16 | contract { 17 | returns() implies value 18 | } 19 | if (!value) { 20 | val message = lazyErrorMessage() 21 | kspLogger?.error("\nKtorfitx 编译期错误检查: $message\n详情请查看官方文档: $KTORFITX_DOCUMENT_URL", this) 22 | compileCheckError(message, this) 23 | } 24 | } 25 | 26 | private const val KTORFITX_DOCUMENT_URL = "https://vividcodex.github.io/ktorfitx-document/index_md.html" -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/check/KtorfitxCompileCheckException.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.check 2 | 3 | import com.google.devtools.ksp.symbol.FileLocation 4 | import com.google.devtools.ksp.symbol.KSNode 5 | 6 | /** 7 | * Ktorfitx 编译异常 8 | */ 9 | private class KtorfitxCompileCheckException( 10 | message: String, 11 | ) : IllegalStateException(message) 12 | 13 | /** 14 | * 编译检查错误 15 | */ 16 | internal fun compileCheckError( 17 | message: String, 18 | ksNode: T, 19 | ): Nothing { 20 | val errorLocation = (ksNode.location as? FileLocation) 21 | ?.let { "\n错误位于:${it.filePath}:${it.lineNumber}" } 22 | ?: "" 23 | throw KtorfitxCompileCheckException("$message$errorLocation") 24 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/constants/KtorQualifiers.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.constants 2 | 3 | /** 4 | * Ktor 相关全类名 5 | */ 6 | internal object KtorQualifiers { 7 | 8 | const val KTOR = "io.ktor" 9 | 10 | const val PACKAGE_HTTP = "io.ktor.http" 11 | 12 | private const val PACKAGE_CLIENT = "io.ktor.client" 13 | 14 | const val PACKAGE_REQUEST = "io.ktor.client.request" 15 | 16 | const val PACKAGE_REQUEST_FORMS = "io.ktor.client.request.forms" 17 | 18 | const val HTTP_CLIENT = "$PACKAGE_CLIENT.HttpClient" 19 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/constants/KtorfitxQualifiers.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.constants 2 | 3 | /** 4 | * Ktorfitx 相关全类名 5 | */ 6 | internal object KtorfitxQualifiers { 7 | 8 | private const val PACKAGE_ANNOTATION = "cn.vividcode.multiplatform.ktorfitx.annotation" 9 | 10 | const val PACKAGE_API = "cn.vividcode.multiplatform.ktorfitx.api" 11 | 12 | const val PACKAGE_API_EXPENDS = "cn.vividcode.multiplatform.ktorfitx.api.expends" 13 | 14 | const val PACKAGE_API_MOCK = "cn.vividcode.multiplatform.ktorfitx.api.mock" 15 | 16 | const val API = "$PACKAGE_ANNOTATION.Api" 17 | 18 | const val BODY = "$PACKAGE_ANNOTATION.Body" 19 | 20 | const val FORM = "$PACKAGE_ANNOTATION.Form" 21 | 22 | const val HEADER = "$PACKAGE_ANNOTATION.Header" 23 | 24 | const val PATH = "$PACKAGE_ANNOTATION.Path" 25 | 26 | const val QUERY = "$PACKAGE_ANNOTATION.Query" 27 | 28 | const val EXCEPTION_LISTENERS = "$PACKAGE_ANNOTATION.ExceptionListeners" 29 | 30 | const val EXCEPTION_LISTENER = "$PACKAGE_API.exception.ExceptionListener" 31 | 32 | const val RESULT_BODY = "$PACKAGE_API.model.ResultBody" 33 | 34 | const val API_SCOPE = "$PACKAGE_API.scope.ApiScope" 35 | 36 | const val DEFAULT_API_SCOPE = "$PACKAGE_API.scope.DefaultApiScope" 37 | 38 | const val KTORFIT = "$PACKAGE_API.Ktorfit" 39 | 40 | const val KTORFIT_CONFIG = "$PACKAGE_API.config.KtorfitConfig" 41 | 42 | const val MOCK = "$PACKAGE_ANNOTATION.Mock" 43 | 44 | const val MOCK_CLIENT = "$PACKAGE_API_MOCK.MockClient" 45 | 46 | const val MOCK_PROVIDER = "$PACKAGE_API_MOCK.MockProvider" 47 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/expends/ClassKindExpends.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.expends 2 | 3 | import com.google.devtools.ksp.symbol.ClassKind 4 | 5 | /** 6 | * 获取 ClassKind 的代码 7 | */ 8 | internal val ClassKind.code: String 9 | get() = this.type.replace('_', ' ') -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/expends/KSPSymbolExpends.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.expends 2 | 3 | import com.google.devtools.ksp.KspExperimental 4 | import com.google.devtools.ksp.getAnnotationsByType 5 | import com.google.devtools.ksp.symbol.KSAnnotated 6 | import com.google.devtools.ksp.symbol.KSAnnotation 7 | import com.google.devtools.ksp.symbol.KSClassDeclaration 8 | import com.google.devtools.ksp.symbol.KSType 9 | import com.squareup.kotlinpoet.ClassName 10 | import com.squareup.kotlinpoet.ParameterizedTypeName 11 | import com.squareup.kotlinpoet.TypeName 12 | import com.squareup.kotlinpoet.ksp.toClassName 13 | import kotlin.reflect.KClass 14 | import kotlin.reflect.KProperty1 15 | 16 | /** 17 | * 获取 KSAnnotation 18 | */ 19 | internal fun KSAnnotated.getKSAnnotationByType(annotationKClass: KClass): KSAnnotation? { 20 | return this.annotations.filter { 21 | it.shortName.getShortName() == annotationKClass.simpleName && 22 | it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationKClass.qualifiedName 23 | }.firstOrNull() 24 | } 25 | 26 | /** 27 | * 获取 KSAnnotation 28 | */ 29 | internal fun KSAnnotated.getKSAnnotationByType(annotationClassName: ClassName): KSAnnotation? { 30 | return this.annotations.filter { 31 | it.shortName.getShortName() == annotationClassName.simpleName && 32 | it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationClassName.canonicalName 33 | }.firstOrNull() 34 | } 35 | 36 | /** 37 | * 直接获取注解的对象,不支持 KClass 38 | */ 39 | @OptIn(KspExperimental::class) 40 | internal fun KSAnnotated.getAnnotationByType(annotationKClass: KClass): T? { 41 | return this.getAnnotationsByType(annotationKClass).firstOrNull() 42 | } 43 | 44 | /** 45 | * 通过 KSAnnotation 获取参数的 KSClassDeclaration 46 | */ 47 | internal fun KSAnnotation.getArgumentKSClassDeclaration(propertyName: String): KSClassDeclaration? { 48 | val value = this.arguments.find { it.name?.asString() == propertyName }?.value 49 | check(value is KSType) { "$value is not KSType!" } 50 | return value.declaration as KSClassDeclaration 51 | } 52 | 53 | /** 54 | * 获取注解上的数据 55 | */ 56 | internal fun KSAnnotation.getValue(property: KProperty1<*, V>): V? { 57 | return this.getValue(property.name) 58 | } 59 | 60 | /** 61 | * 获取注解上的数据 62 | */ 63 | @Suppress("UNCHECKED_CAST") 64 | internal fun KSAnnotation.getValue(propertyName: String): V? { 65 | val value = this.arguments.find { it.name?.asString() == propertyName }?.value 66 | check(value !is KSType && value !is ArrayList<*>) { 67 | "此方法不支持此类型" 68 | } 69 | return value as? V 70 | } 71 | 72 | /** 73 | * 获取注解上的数据 74 | */ 75 | internal inline fun KSAnnotation.getValues(property: KProperty1<*, Array>): Array? { 76 | return this.getValues(property.name) 77 | } 78 | 79 | /** 80 | * 获取注解上的数组数据 81 | */ 82 | internal inline fun KSAnnotation.getValues(propertyName: String): Array? { 83 | val values = this.arguments.find { it.name?.asString() == propertyName }?.value 84 | return if (values is ArrayList<*>) { 85 | values.map { it as T }.toTypedArray() 86 | } else null 87 | } 88 | 89 | /** 90 | * 获取注解上的 KClass 的 ClassName 91 | */ 92 | internal fun KSAnnotation.getClassName(propertyName: String): ClassName? { 93 | val value = this.arguments.find { it.name?.asString() == propertyName }?.value ?: return null 94 | if (value is KSClassDeclaration) { 95 | return value.toClassName() 96 | } else if (value is KSType) { 97 | return (value.declaration as KSClassDeclaration).toClassName() 98 | } else { 99 | error("$value is not a KSClassDeclaration or KSType") 100 | } 101 | } 102 | 103 | /** 104 | * 获取注解上的 KClass 的 ClassName 105 | */ 106 | internal fun KSAnnotation.getClassNames(propertyName: String): Array? { 107 | val values = this.arguments.find { it.name?.asString() == propertyName }?.value 108 | return if (values is ArrayList<*>) { 109 | values.map { 110 | check(it is KSType) { "$it is not KSType!" } 111 | (it.declaration as KSClassDeclaration).toClassName() 112 | }.toTypedArray() 113 | } else null 114 | } 115 | 116 | @Suppress("UNCHECKED_CAST", "unused") 117 | internal fun Any.safeAs(): T = this as T 118 | 119 | /** 120 | * 获取TypeName上用到的所有ClassName 121 | */ 122 | internal val TypeName.classNames: List 123 | get() = when (this) { 124 | is ClassName -> listOf(this) 125 | is ParameterizedTypeName -> this.classNames 126 | else -> error("不支持的类型 $simpleName") 127 | } 128 | 129 | private val ParameterizedTypeName.classNames: List 130 | get() = buildList { 131 | this += rawType 132 | typeArguments.forEach { 133 | if (it is ClassName) { 134 | this += it 135 | } else if (it is ParameterizedTypeName) { 136 | this += it.classNames 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * 获取 TypeName 上的 simpleName 143 | */ 144 | internal val TypeName.simpleName: String 145 | get() = when (this) { 146 | is ParameterizedTypeName -> this.simpleName 147 | is ClassName -> this.simpleName 148 | else -> this.toString() 149 | } 150 | 151 | private val ParameterizedTypeName.simpleName: String 152 | get() = buildString { 153 | append(rawType.simpleName) 154 | if (typeArguments.isNotEmpty()) { 155 | append("<") 156 | val code = typeArguments.joinToString { 157 | when (it) { 158 | is ClassName -> it.simpleName 159 | is ParameterizedTypeName -> it.simpleName 160 | else -> it.toString() 161 | } 162 | } 163 | append(code) 164 | append(">") 165 | } 166 | } 167 | 168 | /** 169 | * 获取 TypeName 的 rawType 170 | */ 171 | internal val TypeName.rawType: ClassName 172 | get() = when (this) { 173 | is ParameterizedTypeName -> { 174 | this.rawType.copy(this.isNullable, emptyList(), emptyMap()) 175 | } 176 | 177 | is ClassName -> this 178 | else -> error("不允许使用 rawType 属性") 179 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/expends/KotlinPoetExpends.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.expends 2 | 3 | import com.squareup.kotlinpoet.* 4 | import kotlin.reflect.KClass 5 | 6 | /** 7 | * buildFileSpec 8 | */ 9 | internal fun buildFileSpec( 10 | className: ClassName, 11 | block: FileSpec.Builder.() -> Unit = {}, 12 | ): FileSpec = FileSpec.builder(className).apply(block).build() 13 | 14 | /** 15 | * buildClassTypeSpec 16 | */ 17 | internal fun buildClassTypeSpec( 18 | className: ClassName, 19 | block: TypeSpec.Builder.() -> Unit = {}, 20 | ): TypeSpec = TypeSpec.classBuilder(className).apply(block).build() 21 | 22 | /** 23 | * buildCompanionObjectTypeSpec 24 | */ 25 | internal fun buildCompanionObjectTypeSpec( 26 | block: TypeSpec.Builder.() -> Unit = {}, 27 | ): TypeSpec = TypeSpec.companionObjectBuilder().apply(block).build() 28 | 29 | /** 30 | * buildFunSpec 31 | */ 32 | internal fun buildFunSpec( 33 | name: String, 34 | block: FunSpec.Builder.() -> Unit = {}, 35 | ): FunSpec = FunSpec.builder(name).apply(block).build() 36 | 37 | /** 38 | * buildConstructorFunSpec 39 | */ 40 | internal fun buildConstructorFunSpec( 41 | block: FunSpec.Builder.() -> Unit = {}, 42 | ): FunSpec = FunSpec.constructorBuilder().apply(block).build() 43 | 44 | /** 45 | * buildGetterFunSpec 46 | */ 47 | internal fun buildGetterFunSpec( 48 | block: FunSpec.Builder.() -> Unit = {}, 49 | ): FunSpec = FunSpec.getterBuilder().apply(block).build() 50 | 51 | /** 52 | * buildPropertySpec 53 | */ 54 | internal fun buildPropertySpec( 55 | name: String, 56 | type: KClass<*>, 57 | vararg modifiers: KModifier, 58 | block: PropertySpec.Builder.() -> Unit = {}, 59 | ): PropertySpec = PropertySpec.builder(name, type, *modifiers).apply(block).build() 60 | 61 | /** 62 | * buildPropertySpec 63 | */ 64 | internal fun buildPropertySpec( 65 | name: String, 66 | type: TypeName, 67 | vararg modifiers: KModifier, 68 | block: PropertySpec.Builder.() -> Unit = {}, 69 | ): PropertySpec = PropertySpec.builder(name, type, *modifiers).apply(block).build() 70 | 71 | /** 72 | * buildParameterSpec 73 | */ 74 | internal fun buildParameterSpec( 75 | name: String, 76 | type: TypeName, 77 | block: ParameterSpec.Builder.() -> Unit = {}, 78 | ): ParameterSpec = ParameterSpec.builder(name, type).apply(block).build() 79 | 80 | /** 81 | * buildAnnotationSpec 82 | */ 83 | internal fun buildAnnotationSpec( 84 | type: KClass, 85 | block: AnnotationSpec.Builder.() -> Unit = {}, 86 | ): AnnotationSpec = AnnotationSpec.builder(type).apply(block).build() -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/expends/StringExpends.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.expends 2 | 3 | private val LowerCamelCaseRegex by lazy { 4 | "^[a-z][a-zA-Z0-9]*$".toRegex() 5 | } 6 | 7 | private val UpperCamelCaseRegex by lazy { 8 | "^[A-Z][a-zA-Z0-9]*$".toRegex() 9 | } 10 | 11 | /** 12 | * 判断是否是小驼峰命名 13 | */ 14 | internal fun String.isLowerCamelCase(): Boolean { 15 | return LowerCamelCaseRegex.matches(this) 16 | } 17 | 18 | /** 19 | * 判断是否是大驼峰命名 20 | */ 21 | internal fun String.isUpperCamelCase(): Boolean { 22 | return UpperCamelCaseRegex.matches(this) 23 | } 24 | 25 | /** 26 | * 改为小驼峰命名 27 | */ 28 | internal fun String.lowerCamelCase(): String { 29 | return when { 30 | '_' in this -> { 31 | this.split('_').joinToString("") { 32 | it.replaceFirstToUppercase() 33 | }.replaceFirstToLowercase() 34 | } 35 | 36 | this[0].isUpperCase() -> this.replaceFirstToLowercase() 37 | else -> this 38 | } 39 | } 40 | 41 | /** 42 | * 改为大驼峰命名 43 | */ 44 | internal fun String.upperCamelCase(): String { 45 | return when { 46 | '_' in this -> { 47 | this.split('_').joinToString { 48 | it.replaceFirstToUppercase() 49 | } 50 | } 51 | 52 | this[0].isLowerCase() -> this.replaceFirstToUppercase() 53 | else -> this 54 | } 55 | } 56 | 57 | /** 58 | * 替换首字母为小写 59 | */ 60 | internal fun String.replaceFirstToLowercase(): String { 61 | return this.replaceFirstChar { it.lowercaseChar() } 62 | } 63 | 64 | /** 65 | * 替换首字母为大写 66 | */ 67 | internal fun String.replaceFirstToUppercase(): String { 68 | return this.replaceFirstChar { it.uppercaseChar() } 69 | } 70 | 71 | /** 72 | * 是否开头是http或https 73 | */ 74 | internal fun String.isHttpOrHttps(): Boolean { 75 | return this.startsWith("http://") || this.startsWith("https://") 76 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/ApiKotlinPoet.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorQualifiers 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.* 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block.CodeBlockBuilder 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block.HttpClientCodeBlock 8 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block.MockClientCodeBlock 9 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block.UseImports 10 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.MockModel 11 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ParameterModel 12 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ClassStructure 13 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.FunStructure 14 | import com.squareup.kotlinpoet.* 15 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 16 | 17 | /** 18 | * 项目名称:ktorfitx 19 | * 20 | * 作者昵称:li-jia-wei 21 | * 22 | * 创建日期:2024/7/1 21:20 23 | * 24 | * 文件介绍:ApiKotlinPoet 25 | */ 26 | internal class ApiKotlinPoet { 27 | 28 | private var hasMockClient = false 29 | private var hasHttpClient = false 30 | 31 | private companion object { 32 | 33 | private val ktorfitConfigClassName = ClassName.bestGuess(KtorfitxQualifiers.KTORFIT_CONFIG) 34 | 35 | private val ktorfitClassName = ClassName.bestGuess(KtorfitxQualifiers.KTORFIT) 36 | 37 | private val httpClientClassName = ClassName.bestGuess(KtorQualifiers.HTTP_CLIENT) 38 | 39 | private val mockClientClassName = ClassName.bestGuess(KtorfitxQualifiers.MOCK_CLIENT) 40 | } 41 | 42 | /** 43 | * 文件 44 | */ 45 | fun getFileSpec(classStructure: ClassStructure): FileSpec { 46 | initFunStructuresStatus(classStructure.funStructures) 47 | return buildFileSpec(classStructure.className) { 48 | indent("\t") 49 | addType(getTypeSpec(classStructure)) 50 | addProperties(getExpendPropertySpecs(classStructure)) 51 | UseImports.get().forEach(::addImport) 52 | } 53 | } 54 | 55 | private fun initFunStructuresStatus(funStructures: List) { 56 | this.hasMockClient = funStructures.any { funStructure -> 57 | funStructure.functionModels.any { it is MockModel } 58 | } 59 | this.hasHttpClient = funStructures.any { funStructure -> 60 | funStructure.functionModels.all { it !is MockModel } 61 | } 62 | } 63 | 64 | /** 65 | * 实现类 66 | */ 67 | private fun getTypeSpec(classStructure: ClassStructure): TypeSpec { 68 | val primaryConstructorFunSpec = buildConstructorFunSpec { 69 | addModifiers(KModifier.PRIVATE) 70 | if (hasHttpClient || hasMockClient) { 71 | addParameter("ktorfit", ktorfitConfigClassName) 72 | } 73 | if (hasHttpClient) { 74 | addParameter("httpClient", httpClientClassName) 75 | } 76 | if (hasMockClient) { 77 | addParameter("mockClient", mockClientClassName) 78 | } 79 | } 80 | return buildClassTypeSpec(classStructure.className) { 81 | addModifiers(KModifier.PRIVATE) 82 | addSuperinterface(classStructure.superinterface) 83 | primaryConstructor(primaryConstructorFunSpec) 84 | if (hasHttpClient || hasMockClient) { 85 | val ktorfitConfigPropertySpec = buildPropertySpec("ktorfit", ktorfitConfigClassName, KModifier.PRIVATE) { 86 | initializer("ktorfit") 87 | mutable(false) 88 | } 89 | addProperty(ktorfitConfigPropertySpec) 90 | } 91 | if (hasHttpClient) { 92 | val httpClientPropertySpec = buildPropertySpec("httpClient", httpClientClassName, KModifier.PRIVATE) { 93 | initializer("httpClient") 94 | mutable(false) 95 | } 96 | addProperty(httpClientPropertySpec) 97 | } 98 | if (hasMockClient) { 99 | val mockClientPropertySpec = buildPropertySpec("mockClient", mockClientClassName, KModifier.PRIVATE) { 100 | initializer("mockClient") 101 | mutable(false) 102 | } 103 | addProperty(mockClientPropertySpec) 104 | } 105 | addType(getCompanionObjectBuilder(classStructure)) 106 | addFunctions(getFunSpecs(classStructure)) 107 | } 108 | } 109 | 110 | /** 111 | * 伴生对象 112 | */ 113 | private fun getCompanionObjectBuilder(classStructure: ClassStructure): TypeSpec { 114 | val type = classStructure.superinterface.copy(nullable = true) 115 | val propertySpec = buildPropertySpec("instance", type, KModifier.PRIVATE) { 116 | initializer("null") 117 | mutable(true) 118 | } 119 | val codeBlock = buildCodeBlock { 120 | val simpleName = classStructure.className.simpleName 121 | val parameters = mutableListOf() 122 | if (hasHttpClient || hasMockClient) { 123 | parameters += "ktorfit.config" 124 | } 125 | if (hasHttpClient) { 126 | parameters += "ktorfit.config.httpClient!!" 127 | } 128 | if (hasMockClient) { 129 | parameters += "ktorfit.config.mockClient!!" 130 | } 131 | beginControlFlow("return instance ?: $simpleName(${parameters.joinToString()}).also") 132 | addStatement("instance = it") 133 | endControlFlow() 134 | } 135 | val funSpecs = classStructure.apiStructure.apiScopeClassNames.map { apiScopeClassName -> 136 | val jvmNameAnnotationSpec = buildAnnotationSpec(JvmName::class) { 137 | addMember("%S", "getInstanceBy${apiScopeClassName.simpleName}") 138 | } 139 | buildFunSpec("getInstance") { 140 | addAnnotation(jvmNameAnnotationSpec) 141 | addModifiers(classStructure.kModifier) 142 | returns(classStructure.superinterface) 143 | if (hasHttpClient || hasMockClient) { 144 | addParameter( 145 | "ktorfit", 146 | ktorfitClassName.parameterizedBy(apiScopeClassName) 147 | ) 148 | } 149 | addCode(codeBlock) 150 | } 151 | } 152 | return buildCompanionObjectTypeSpec { 153 | addModifiers(classStructure.kModifier) 154 | addProperty(propertySpec) 155 | addFunctions(funSpecs) 156 | } 157 | } 158 | 159 | /** 160 | * 扩展函数 161 | */ 162 | private fun getExpendPropertySpecs(classStructure: ClassStructure): List { 163 | val expendPropertyName = classStructure.superinterface.simpleName.replaceFirstChar { it.lowercase() } 164 | return classStructure.apiStructure.apiScopeClassNames.map { apiScopeClassName -> 165 | UseImports += apiScopeClassName 166 | val jvmNameAnnotationSpec = buildAnnotationSpec(JvmName::class) { 167 | addMember("%S", "${expendPropertyName}By${apiScopeClassName.simpleName}") 168 | } 169 | val getterFunSpec = buildGetterFunSpec { 170 | addAnnotation(jvmNameAnnotationSpec) 171 | val simpleName = classStructure.className.simpleName 172 | val parameter = if (hasHttpClient || hasMockClient) "this" else "" 173 | addStatement("return $simpleName.getInstance($parameter)") 174 | } 175 | buildPropertySpec(expendPropertyName, classStructure.superinterface, classStructure.kModifier) { 176 | receiver(ktorfitClassName.parameterizedBy(apiScopeClassName)) 177 | getter(getterFunSpec) 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * 实现方法 184 | */ 185 | private fun getFunSpecs(classStructure: ClassStructure): List { 186 | return classStructure.funStructures.map { 187 | buildFunSpec(it.funName) { 188 | addModifiers(KModifier.SUSPEND, KModifier.OVERRIDE) 189 | addParameters(getParameterSpecs(it.parameterModels)) 190 | addCode(getCodeBlock(classStructure, it)) 191 | val returnStructure = it.returnStructure 192 | UseImports += returnStructure.classNames 193 | returns(returnStructure.typeName) 194 | } 195 | }.toList() 196 | } 197 | 198 | private fun getParameterSpecs(models: List): List { 199 | return models.map { buildParameterSpec(it.varName, it.typeName) } 200 | } 201 | 202 | private fun getCodeBlock(classStructure: ClassStructure, funStructure: FunStructure): CodeBlock { 203 | return buildCodeBlock { 204 | val isMockClient = funStructure.functionModels.any { it is MockModel } 205 | val codeBlockKClass = if (isMockClient) MockClientCodeBlock::class else HttpClientCodeBlock::class 206 | with(CodeBlockBuilder(classStructure, funStructure, codeBlockKClass)) { 207 | buildCodeBlock() 208 | } 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/ReturnTypes.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.asClassName 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/8/18 01:44 13 | * 14 | * 文件介绍:ReturnTypes 15 | */ 16 | internal object ReturnTypes { 17 | 18 | val unitClassName = Unit::class.asClassName() 19 | 20 | val byteArrayClassName = ByteArray::class.asClassName() 21 | 22 | val resultBodyClassName = ClassName.bestGuess(KtorfitxQualifiers.RESULT_BODY) 23 | 24 | val stringClassName = String::class.asClassName() 25 | 26 | val returnTypes = arrayOf( 27 | unitClassName, 28 | byteArrayClassName, 29 | resultBodyClassName, 30 | stringClassName 31 | ) 32 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/block/ClientCodeBlock.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.* 4 | import com.squareup.kotlinpoet.CodeBlock 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/8/14 17:30 12 | * 13 | * 文件介绍:ClientCodeBlock 14 | */ 15 | internal sealed interface ClientCodeBlock { 16 | 17 | /** 18 | * httpClient or mockClient 19 | */ 20 | fun CodeBlock.Builder.buildClientCodeBlock( 21 | funName: String, 22 | url: String, 23 | hasBuilder: Boolean, 24 | builder: CodeBlock.Builder.() -> Unit 25 | ) 26 | 27 | /** 28 | * bearerAuth 29 | */ 30 | fun CodeBlock.Builder.buildBearerAuthCodeBlock() 31 | 32 | /** 33 | * headers 34 | */ 35 | fun CodeBlock.Builder.buildHeadersCodeBlock( 36 | headersModel: HeadersModel?, 37 | headerModels: List 38 | ) 39 | 40 | /** 41 | * queries 42 | */ 43 | fun CodeBlock.Builder.buildQueriesCodeBlock(queryModels: List) 44 | 45 | /** 46 | * forms 47 | */ 48 | fun CodeBlock.Builder.buildFormsCodeBlock(formModels: List) 49 | 50 | /** 51 | * body 52 | */ 53 | fun CodeBlock.Builder.buildBodyCodeBlock(bodyModel: BodyModel) 54 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/block/CodeBlockBuilder.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.isHttpOrHttps 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.simpleName 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.ReturnTypes 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.* 8 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ClassStructure 9 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.FunStructure 10 | import com.squareup.kotlinpoet.ClassName 11 | import com.squareup.kotlinpoet.CodeBlock 12 | import kotlin.reflect.KClass 13 | 14 | /** 15 | * 项目名称:ktorfitx 16 | * 17 | * 作者昵称:li-jia-wei 18 | * 19 | * 创建日期:2024/8/14 17:24 20 | * 21 | * 文件介绍:CodeBlockBuilder 22 | */ 23 | internal class CodeBlockBuilder( 24 | private val classStructure: ClassStructure, 25 | private val funStructure: FunStructure, 26 | private val codeBlockKClass: KClass, 27 | ) { 28 | 29 | private val returnStructure = funStructure.returnStructure 30 | private val valueParameterModels = funStructure.valueParameterModels 31 | private val functionModels = funStructure.functionModels 32 | 33 | private companion object { 34 | private val exceptionClassNames = arrayOf( 35 | ClassName.bestGuess("kotlin.Exception"), 36 | ClassName.bestGuess("java.lang.Exception"), 37 | ) 38 | } 39 | 40 | fun CodeBlock.Builder.buildCodeBlock() = with(getClientCodeBlock()) { 41 | buildExceptionCodeBlock { 42 | val apiModel = functionModels.first { it is ApiModel } as ApiModel 43 | val funName = apiModel.requestFunName 44 | val fullUrl = parseToFullUrl(apiModel.url) 45 | val isNeedClientBuilder = isNeedClientBuilder() 46 | buildClientCodeBlock(funName, fullUrl, isNeedClientBuilder) { 47 | val bearerAuth = functionModels.any { it is BearerAuthModel } 48 | if (bearerAuth) { 49 | buildBearerAuthCodeBlock() 50 | } 51 | val headersModel = functionModels.find { it is HeadersModel } as? HeadersModel 52 | val headerModels = valueParameterModels.filterIsInstance() 53 | if (headerModels.isNotEmpty() || headersModel != null) { 54 | buildHeadersCodeBlock(headersModel, headerModels) 55 | } 56 | val queryModels = valueParameterModels.filterIsInstance() 57 | if (queryModels.isNotEmpty()) { 58 | buildQueriesCodeBlock(queryModels) 59 | } 60 | val formModels = valueParameterModels.filterIsInstance() 61 | if (formModels.isNotEmpty()) { 62 | buildFormsCodeBlock(formModels) 63 | } 64 | if (this@with is MockClientCodeBlock) { 65 | val pathModels = valueParameterModels.filterIsInstance() 66 | if (pathModels.isNotEmpty()) { 67 | buildPathsCodeBlock(pathModels) 68 | } 69 | } 70 | val bodyModel = valueParameterModels.find { it is BodyModel } as? BodyModel 71 | if (bodyModel != null) { 72 | UseImports += bodyModel.typeQualifiedName 73 | buildBodyCodeBlock(bodyModel) 74 | } 75 | } 76 | } 77 | } 78 | 79 | private fun CodeBlock.Builder.buildExceptionCodeBlock( 80 | builder: CodeBlock.Builder.() -> Unit, 81 | ) { 82 | beginControlFlow(if (returnStructure.rawType != ReturnTypes.unitClassName) "return try" else "try") 83 | builder() 84 | val exceptionListenerModels = functionModels.filterIsInstance() 85 | exceptionListenerModels.forEach { 86 | UseImports += it.exceptionTypeName 87 | UseImports += it.listenerClassName 88 | nextControlFlow("catch (e: ${it.exceptionTypeName.simpleName})") 89 | beginControlFlow("with(${it.listenerClassName.simpleName})") 90 | val superinterfaceName = classStructure.superinterface.simpleName 91 | val funName = funStructure.funName 92 | addStatement("$superinterfaceName::$funName.onExceptionListener(e)") 93 | endControlFlow() 94 | if (it.returnTypeName == ReturnTypes.unitClassName) { 95 | buildExceptionReturnCodeBlock() 96 | } 97 | } 98 | if (exceptionListenerModels.all { it.exceptionTypeName !in exceptionClassNames }) { 99 | if (returnStructure.rawType == ReturnTypes.resultBodyClassName) { 100 | nextControlFlow("catch (e: Exception)") 101 | } else { 102 | nextControlFlow("catch (_: Exception)") 103 | } 104 | buildExceptionReturnCodeBlock() 105 | } 106 | endControlFlow() 107 | } 108 | 109 | private fun CodeBlock.Builder.buildExceptionReturnCodeBlock() { 110 | if (returnStructure.isNullable) { 111 | addStatement("null") 112 | return 113 | } 114 | when (returnStructure.rawType) { 115 | ReturnTypes.resultBodyClassName -> { 116 | addStatement("ResultBody.exception(e)") 117 | } 118 | 119 | ReturnTypes.byteArrayClassName -> { 120 | addStatement("ByteArray(0)") 121 | } 122 | 123 | ReturnTypes.stringClassName -> { 124 | addStatement("\"\"") 125 | } 126 | } 127 | } 128 | 129 | private fun getClientCodeBlock(): ClientCodeBlock { 130 | return when (this.codeBlockKClass) { 131 | HttpClientCodeBlock::class -> { 132 | HttpClientCodeBlock(classStructure.className, returnStructure) 133 | } 134 | 135 | MockClientCodeBlock::class -> { 136 | val mockModel = funStructure.functionModels.first { it is MockModel } as MockModel 137 | MockClientCodeBlock(classStructure.className, mockModel) 138 | } 139 | 140 | else -> error("不支持的类型") 141 | } 142 | } 143 | 144 | private fun parseToFullUrl(url: String): String { 145 | val pathModels = valueParameterModels.filterIsInstance() 146 | val initialUrl = if (url.isHttpOrHttps()) url else { 147 | val apiUrl = classStructure.apiStructure.url 148 | if (apiUrl == "") return url 149 | val url = url.removePrefix("/") 150 | "$apiUrl/$url" 151 | } 152 | val fullUrl = pathModels.fold(initialUrl) { acc, it -> 153 | it.valueParameter.compileCheck(url.contains("{${it.name}}")) { 154 | val funName = funStructure.funName 155 | "$funName 方法上 ${it.varName} 参数上的 @Path 注解的 name 参数没有在 url 上找到" 156 | } 157 | acc.replace("{${it.name}}", "\${${it.varName}}") 158 | } 159 | return fullUrl 160 | } 161 | 162 | private fun isNeedClientBuilder(): Boolean { 163 | return valueParameterModels.any { it !is PathModel } || functionModels.any { it is BearerAuthModel || it is HeadersModel } 164 | } 165 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/block/HttpClientCodeBlock.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorQualifiers 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.ReturnTypes 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.* 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ReturnStructure 8 | import com.squareup.kotlinpoet.ClassName 9 | import com.squareup.kotlinpoet.CodeBlock 10 | 11 | /** 12 | * 项目名称:ktorfitx 13 | * 14 | * 作者昵称:li-jia-wei 15 | * 16 | * 创建日期:2024/8/14 17:31 17 | * 18 | * 文件介绍:HttpClientCodeBlock 19 | */ 20 | internal class HttpClientCodeBlock( 21 | private val className: ClassName, 22 | private val returnStructure: ReturnStructure, 23 | ) : ClientCodeBlock { 24 | 25 | override fun CodeBlock.Builder.buildClientCodeBlock( 26 | funName: String, 27 | url: String, 28 | hasBuilder: Boolean, 29 | builder: CodeBlock.Builder.() -> Unit, 30 | ) { 31 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, funName) 32 | val httpClientCode = "this.httpClient.$funName(\"$url\")" 33 | if (hasBuilder) { 34 | beginControlFlow(httpClientCode) 35 | builder() 36 | endControlFlow() 37 | if (returnStructure.rawType != ReturnTypes.unitClassName) { 38 | addStatement(returnFunName) 39 | } 40 | } else { 41 | addStatement(httpClientCode + returnFunName) 42 | } 43 | } 44 | 45 | private val returnFunName: String 46 | get() { 47 | var funName = when (returnStructure.notNullRawType) { 48 | ReturnTypes.unitClassName -> null 49 | ReturnTypes.resultBodyClassName -> "safeResultBody" 50 | ReturnTypes.byteArrayClassName -> "safeByteArray" 51 | ReturnTypes.stringClassName -> "safeText" 52 | else -> null 53 | } 54 | if (funName != null) { 55 | if (returnStructure.isNullable) { 56 | funName += "OrNull" 57 | } 58 | UseImports.addImports(KtorfitxQualifiers.PACKAGE_API_EXPENDS, funName) 59 | } 60 | return funName?.let { ".$funName()" } ?: "" 61 | } 62 | 63 | override fun CodeBlock.Builder.buildBearerAuthCodeBlock() { 64 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, "bearerAuth") 65 | addStatement("this@${className.simpleName}.ktorfit.token?.invoke()?.let { bearerAuth(it) }") 66 | } 67 | 68 | override fun CodeBlock.Builder.buildHeadersCodeBlock( 69 | headersModel: HeadersModel?, 70 | headerModels: List, 71 | ) { 72 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, "headers") 73 | beginControlFlow("headers") 74 | headersModel?.headerMap?.forEach { (name, value) -> 75 | addStatement("append(\"$name\", \"$value\")") 76 | } 77 | headerModels.forEach { 78 | addStatement("append(\"${it.name}\", ${it.varName})") 79 | } 80 | endControlFlow() 81 | } 82 | 83 | override fun CodeBlock.Builder.buildQueriesCodeBlock(queryModels: List) { 84 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, "parameter") 85 | queryModels.forEach { 86 | addStatement("parameter(\"${it.name}\", ${it.varName})") 87 | } 88 | } 89 | 90 | override fun CodeBlock.Builder.buildFormsCodeBlock(formModels: List) { 91 | UseImports.addImports(KtorQualifiers.PACKAGE_HTTP, "contentType", "ContentType") 92 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, "setBody") 93 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST_FORMS, "formData", "MultiPartFormDataContent") 94 | addStatement("contentType(ContentType.MultiPart.FormData)") 95 | beginControlFlow("formData {") 96 | formModels.forEach { 97 | addStatement("append(\"${it.name}\", ${it.varName})") 98 | } 99 | nextControlFlow(".let") 100 | addStatement("setBody(MultiPartFormDataContent(it))") 101 | endControlFlow() 102 | } 103 | 104 | override fun CodeBlock.Builder.buildBodyCodeBlock(bodyModel: BodyModel) { 105 | UseImports.addImports(KtorQualifiers.PACKAGE_HTTP, "contentType", "ContentType") 106 | UseImports.addImports(KtorQualifiers.PACKAGE_REQUEST, "setBody") 107 | addStatement("contentType(ContentType.Application.Json)") 108 | addStatement("setBody(${bodyModel.varName})") 109 | } 110 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/block/MockClientCodeBlock.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.* 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.CodeBlock 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/8/14 17:31 13 | * 14 | * 文件介绍:MockClientCodeBlock 15 | */ 16 | internal class MockClientCodeBlock( 17 | private val className: ClassName, 18 | private val mockModel: MockModel, 19 | ) : ClientCodeBlock { 20 | 21 | override fun CodeBlock.Builder.buildClientCodeBlock( 22 | funName: String, 23 | url: String, 24 | hasBuilder: Boolean, 25 | builder: CodeBlock.Builder.() -> Unit, 26 | ) { 27 | UseImports += mockModel.provider 28 | UseImports.addImports(mockModel.status.packageName, mockModel.status.simpleNames.first()) 29 | val provider = mockModel.provider.simpleName 30 | val status = "MockStatus.${mockModel.status.simpleName}" 31 | val leftRound = mockModel.delayRange[0] 32 | val rightRound = mockModel.delayRange.let { if (it.size == 1) it[0] else it[1] } 33 | val delayRange = "${leftRound}L..${rightRound}L" 34 | val mockClientCode = "this.mockClient.$funName(\"$url\", $provider, $status, $delayRange)" 35 | if (hasBuilder) { 36 | beginControlFlow(mockClientCode) 37 | builder() 38 | endControlFlow() 39 | } else { 40 | addStatement(mockClientCode) 41 | } 42 | } 43 | 44 | override fun CodeBlock.Builder.buildBearerAuthCodeBlock() { 45 | addStatement("this@${className.simpleName}.ktorfit.token?.invoke()?.let { bearerAuth(it) }") 46 | } 47 | 48 | override fun CodeBlock.Builder.buildHeadersCodeBlock( 49 | headersModel: HeadersModel?, 50 | headerModels: List, 51 | ) { 52 | beginControlFlow("headers") 53 | headersModel?.headerMap?.forEach { (name, value) -> 54 | addStatement("append(\"$name\", \"$value\"") 55 | } 56 | headerModels.forEach { 57 | addStatement("append(\"${it.name}\", ${it.varName})") 58 | } 59 | endControlFlow() 60 | } 61 | 62 | override fun CodeBlock.Builder.buildQueriesCodeBlock( 63 | queryModels: List, 64 | ) { 65 | beginControlFlow("queries") 66 | queryModels.forEach { 67 | addStatement("append(\"${it.name}\", ${it.varName})") 68 | } 69 | endControlFlow() 70 | } 71 | 72 | override fun CodeBlock.Builder.buildFormsCodeBlock( 73 | formModels: List, 74 | ) { 75 | beginControlFlow("forms") 76 | formModels.forEach { 77 | addStatement("append(\"${it.name}\", ${it.varName})") 78 | } 79 | endControlFlow() 80 | } 81 | 82 | fun CodeBlock.Builder.buildPathsCodeBlock( 83 | pathModels: List, 84 | ) { 85 | beginControlFlow("paths") 86 | pathModels.forEach { 87 | addStatement("append(\"${it.name}\", ${it.varName})") 88 | } 89 | endControlFlow() 90 | } 91 | 92 | override fun CodeBlock.Builder.buildBodyCodeBlock(bodyModel: BodyModel) { 93 | addStatement("body(${bodyModel.varName})") 94 | } 95 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/kotlinpoet/block/UseImports.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.block 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.classNames 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.ParameterizedTypeName 6 | import com.squareup.kotlinpoet.TypeName 7 | import kotlin.concurrent.getOrSet 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/8/14 23:27 15 | * 16 | * 文件介绍:UseImports 17 | */ 18 | internal object UseImports { 19 | 20 | private val threadLocalImports = ThreadLocal>>() 21 | 22 | fun get(): Map> { 23 | return this.threadLocalImports.get() 24 | } 25 | 26 | fun clear() { 27 | this.threadLocalImports.get()?.clear() 28 | } 29 | 30 | fun addImports(packageName: String, vararg simpleNames: String) { 31 | if (packageName == "kotlin") return 32 | this.threadLocalImports.getOrSet { mutableMapOf() } 33 | .getOrPut(packageName) { mutableSetOf() } 34 | .addAll(simpleNames) 35 | } 36 | 37 | private fun addImportsByClassNames(classNames: List) { 38 | classNames.forEach { 39 | addImports(it.packageName, it.simpleName) 40 | } 41 | } 42 | 43 | private fun addImportByTypeName(typeName: TypeName) { 44 | when (typeName) { 45 | is ClassName -> addImports(typeName.packageName, typeName.simpleName) 46 | is ParameterizedTypeName -> addImportsByClassNames(typeName.classNames) 47 | else -> {} 48 | } 49 | } 50 | 51 | operator fun plusAssign(typeName: TypeName) { 52 | this.addImportByTypeName(typeName) 53 | } 54 | 55 | operator fun plusAssign(classNames: List) { 56 | this.addImportsByClassNames(classNames) 57 | } 58 | 59 | operator fun plusAssign(qualifiedName: String) { 60 | this += ClassName.bestGuess(qualifiedName) 61 | } 62 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/RequestMethod.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.* 4 | import kotlin.reflect.KClass 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/7/1 15:50 12 | * 13 | * 文件介绍:RequestMethod 14 | */ 15 | internal enum class RequestMethod( 16 | val annotation: KClass, 17 | ) { 18 | Get(GET::class), 19 | 20 | Post(POST::class), 21 | 22 | Put(PUT::class), 23 | 24 | Delete(DELETE::class), 25 | 26 | Patch(PATCH::class), 27 | 28 | Options(OPTIONS::class), 29 | 30 | Head(HEAD::class), 31 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/ApiModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/3 14:07 9 | * 10 | * 文件介绍:ApiModel 11 | */ 12 | internal class ApiModel( 13 | val requestFunName: String, 14 | val url: String 15 | ) : FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/BearerAuthModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/8/12 14:52 9 | * 10 | * 文件介绍:BearerAuthModel 11 | */ 12 | internal data object BearerAuthModel : FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/BodyModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 15:55 9 | * 10 | * 文件介绍:BodyModel 11 | */ 12 | internal class BodyModel( 13 | override val varName: String, 14 | val typeQualifiedName: String, 15 | ) : ValueParameterModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/ExceptionListenerModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.TypeName 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/8/13 15:50 12 | * 13 | * 文件介绍:ExceptionListenerModel 14 | */ 15 | internal class ExceptionListenerModel( 16 | val listenerClassName: ClassName, 17 | val exceptionTypeName: TypeName, 18 | val returnTypeName: TypeName, 19 | ) : FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/FormModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 15:56 9 | * 10 | * 文件介绍:FormModel 11 | */ 12 | internal class FormModel( 13 | val name: String, 14 | override val varName: String, 15 | ) : ValueParameterModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/FunctionModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/2 21:22 9 | * 10 | * 文件介绍:FunModel 11 | */ 12 | internal sealed interface FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/HeaderModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 16:02 9 | * 10 | * 文件介绍:HeaderModel 11 | */ 12 | internal class HeaderModel( 13 | val name: String, 14 | override val varName: String, 15 | ) : ValueParameterModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/HeadersModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 16:02 9 | * 10 | * 文件介绍:HeadersModel 11 | */ 12 | internal class HeadersModel( 13 | val headerMap: Map, 14 | ) : FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/MockModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/7/2 21:59 11 | * 12 | * 文件介绍:MockModel 13 | */ 14 | internal class MockModel( 15 | val provider: ClassName, 16 | val status: ClassName, 17 | val delayRange: Array 18 | ) : FunctionModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/ParameterModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | import com.squareup.kotlinpoet.TypeName 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/7/6 9:41 11 | * 12 | * 文件介绍:ParameterModel 13 | */ 14 | internal class ParameterModel( 15 | val varName: String, 16 | val typeName: TypeName, 17 | ) -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/PathModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | import com.google.devtools.ksp.symbol.KSValueParameter 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/7/1 16:14 11 | * 12 | * 文件介绍:PathModel 13 | */ 14 | internal class PathModel( 15 | val name: String, 16 | override val varName: String, 17 | val valueParameter: KSValueParameter 18 | ) : ValueParameterModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/QueryModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 16:14 9 | * 10 | * 文件介绍:QueryModel 11 | */ 12 | internal class QueryModel( 13 | val name: String, 14 | override val varName: String, 15 | ) : ValueParameterModel -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/model/ValueParameterModel.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.model 2 | 3 | /** 4 | * 项目名称:ktorfitx 5 | * 6 | * 作者昵称:li-jia-wei 7 | * 8 | * 创建日期:2024/7/1 15:53 9 | * 10 | * 文件介绍:ValueParameterModel 11 | */ 12 | internal sealed interface ValueParameterModel { 13 | 14 | val varName: String 15 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/structure/ApiStructure.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.structure 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | 5 | /** 6 | * 项目名称:ktorfitx 7 | * 8 | * 作者昵称:li-jia-wei 9 | * 10 | * 创建日期:2024/7/3 14:15 11 | * 12 | * 文件介绍:ApiStructure 13 | */ 14 | internal class ApiStructure( 15 | val url: String, 16 | val apiScopeClassNames: Set, 17 | ) -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/structure/ClassStructure.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.structure 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.KModifier 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/7/1 15:47 12 | * 13 | * 文件介绍:ClassStructure 14 | */ 15 | internal class ClassStructure( 16 | val className: ClassName, 17 | val superinterface: ClassName, 18 | val kModifier: KModifier, 19 | val apiStructure: ApiStructure, 20 | val funStructures: List, 21 | ) -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/structure/FunStructure.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.structure 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.FunctionModel 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ParameterModel 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ValueParameterModel 6 | 7 | /** 8 | * 项目名称:ktorfitx 9 | * 10 | * 作者昵称:li-jia-wei 11 | * 12 | * 创建日期:2024/7/1 15:48 13 | * 14 | * 文件介绍:FunStructure 15 | */ 16 | internal class FunStructure( 17 | val funName: String, 18 | val returnStructure: ReturnStructure, 19 | val parameterModels: List, 20 | val functionModels: List, 21 | val valueParameterModels: List, 22 | ) -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/model/structure/ReturnStructure.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.model.structure 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.classNames 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.rawType 5 | import com.squareup.kotlinpoet.ClassName 6 | import com.squareup.kotlinpoet.ParameterizedTypeName 7 | import com.squareup.kotlinpoet.TypeName 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/7/3 23:04 15 | * 16 | * 文件介绍:ReturnStructure 17 | */ 18 | internal class ReturnStructure( 19 | val typeName: TypeName, 20 | ) { 21 | 22 | val classNames: List by lazy { 23 | when (typeName) { 24 | is ClassName -> listOf(typeName) 25 | is ParameterizedTypeName -> typeName.classNames 26 | else -> error("不支持的返回类型 $typeName") 27 | } 28 | } 29 | 30 | val rawType by lazy { this.typeName.rawType } 31 | 32 | val notNullRawType by lazy { 33 | if (rawType.isNullable) { 34 | this.rawType.copy(nullable = false) 35 | } else this.rawType 36 | } 37 | 38 | val isNullable by lazy { this.typeName.isNullable } 39 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/ApiVisitor.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getClassName 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getClassNames 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 8 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 9 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.ReturnTypes 10 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ApiStructure 11 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ClassStructure 12 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.FunStructure 13 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ReturnStructure 14 | import cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver.ModelResolvers 15 | import com.google.devtools.ksp.getDeclaredFunctions 16 | import com.google.devtools.ksp.getVisibility 17 | import com.google.devtools.ksp.processing.Resolver 18 | import com.google.devtools.ksp.symbol.* 19 | import com.google.devtools.ksp.symbol.Visibility.INTERNAL 20 | import com.google.devtools.ksp.symbol.Visibility.PUBLIC 21 | import com.google.devtools.ksp.visitor.KSEmptyVisitor 22 | import com.squareup.kotlinpoet.ClassName 23 | import com.squareup.kotlinpoet.KModifier 24 | import com.squareup.kotlinpoet.ParameterizedTypeName 25 | import com.squareup.kotlinpoet.ksp.toClassName 26 | import com.squareup.kotlinpoet.ksp.toTypeName 27 | 28 | /** 29 | * 项目名称:ktorfitx 30 | * 31 | * 作者昵称:li-jia-wei 32 | * 33 | * 创建日期:2024/7/1 16:17 34 | * 35 | * 文件介绍:ApiVisitor 36 | */ 37 | internal class ApiVisitor( 38 | private val resolver: Resolver, 39 | ) : KSEmptyVisitor() { 40 | 41 | private companion object { 42 | 43 | private val urlRegex = "^\\S*[a-zA-Z0-9]+\\S*$".toRegex() 44 | 45 | private val apiClassName = ClassName.bestGuess(KtorfitxQualifiers.API) 46 | private val apiScopeClassName by lazy { ClassName.bestGuess(KtorfitxQualifiers.API_SCOPE) } 47 | private val defaultApiScopeClassName by lazy { ClassName.bestGuess(KtorfitxQualifiers.DEFAULT_API_SCOPE) } 48 | } 49 | 50 | override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit): VisitorResult? { 51 | val classStructure = classDeclaration.getClassStructure() ?: return null 52 | return VisitorResult(classStructure) 53 | } 54 | 55 | /** 56 | * 获取 ClassStructure 57 | */ 58 | private fun KSClassDeclaration.getClassStructure(): ClassStructure? { 59 | val apiKSAnnotation = getKSAnnotationByType(apiClassName) ?: return null 60 | val className = ClassName("${packageName.asString()}.impl", "${simpleName.asString()}Impl") 61 | val superinterface = this.toClassName() 62 | val apiScopeClassName = apiKSAnnotation.getClassName("apiScope") ?: defaultApiScopeClassName 63 | val apiScopeClassNames = apiKSAnnotation.getClassNames("apiScopes")?.toSet() ?: emptySet() 64 | val mergeApiScopeClassNames = when { 65 | apiScopeClassName == defaultApiScopeClassName && apiScopeClassNames.isNotEmpty() -> apiScopeClassNames 66 | apiScopeClassName != defaultApiScopeClassName && apiScopeClassNames.isNotEmpty() -> apiScopeClassNames + apiScopeClassName 67 | else -> setOf(apiScopeClassName) 68 | } 69 | apiKSAnnotation.compileCheck(ApiVisitor.apiScopeClassName !in mergeApiScopeClassNames) { 70 | val simpleName = this.simpleName.asString() 71 | "$simpleName 接口上的 @Api 注解的 apiScope 不允许使用 ApiScope::class,请使用默认的 DefaultApiScope::class 或者自定义 object 对象并实现 ApiScope::class" 72 | } 73 | val apiUrl = getApiUrl(apiKSAnnotation) 74 | val apiStructure = ApiStructure(apiUrl, mergeApiScopeClassNames) 75 | val funStructure = getFunStructures() 76 | return ClassStructure(className, superinterface, this.getVisibilityKModifier(), apiStructure, funStructure) 77 | } 78 | 79 | /** 80 | * 获取访问权限的 KModifier 81 | */ 82 | private fun KSClassDeclaration.getVisibilityKModifier(): KModifier { 83 | val visibility = this.getVisibility() 84 | this.compileCheck(visibility == PUBLIC || visibility == INTERNAL) { 85 | val className = this.simpleName.asString() 86 | "$className 接口标记了 @Api,所以必须是 public 或 internal 访问权限的" 87 | } 88 | return KModifier.entries.first { it.name == visibility.name } 89 | } 90 | 91 | /** 92 | * 获取 `@Api` 注解上的 url 参数 93 | */ 94 | private fun KSClassDeclaration.getApiUrl(annotation: KSAnnotation): String { 95 | val apiUrl = annotation.getValue("url") 96 | ?: return "" 97 | if (apiUrl.isBlank()) return "" 98 | if (apiUrl == "/") return "/" 99 | annotation.compileCheck(urlRegex.matches(apiUrl)) { 100 | val className = this.simpleName.asString() 101 | "$className 接口上的 @Api 注解的 url 参数格式错误" 102 | } 103 | return apiUrl 104 | } 105 | 106 | /** 107 | * 获取 FunStructures 108 | */ 109 | private fun KSClassDeclaration.getFunStructures(): List { 110 | return this.getDeclaredFunctions().toList() 111 | .filter { it.isAbstract } 112 | .map { 113 | it.compileCheck(Modifier.SUSPEND in it.modifiers) { 114 | val funName = it.simpleName.asString() 115 | "$funName 方法缺少 suspend 修饰符" 116 | } 117 | val funName = it.simpleName.asString() 118 | val returnStructure = it.getReturnStructure() 119 | val parameterModels = with(ModelResolvers) { it.getAllParameterModel() } 120 | val valueParameterModels = with(ModelResolvers) { it.getAllValueParameterModels() } 121 | val functionModels = with(ModelResolvers) { it.getAllFunctionModels(resolver) } 122 | FunStructure(funName, returnStructure, parameterModels, functionModels, valueParameterModels) 123 | } 124 | } 125 | 126 | /** 127 | * 获取 ReturnStructure 128 | */ 129 | private fun KSFunctionDeclaration.getReturnStructure(): ReturnStructure { 130 | val returnType = this.returnType!! 131 | val typeName = returnType.toTypeName() 132 | val lazyErrorMessage = { 133 | val funName = this.simpleName.asString() 134 | val returnTypes = ReturnTypes.returnTypes.joinToString() 135 | "$funName 方法的返回类型 $typeName 不支持,请使用 $returnTypes" 136 | } 137 | returnType.compileCheck( 138 | value = typeName is ClassName || typeName is ParameterizedTypeName, 139 | lazyErrorMessage = lazyErrorMessage 140 | ) 141 | 142 | if (typeName is ParameterizedTypeName) { 143 | val arguments = typeName.typeArguments 144 | returnType.compileCheck( 145 | value = arguments.size == 1 && (arguments[0] is ClassName || arguments[0] is ParameterizedTypeName), 146 | lazyErrorMessage = lazyErrorMessage 147 | ) 148 | } 149 | val returnStructure = ReturnStructure(typeName) 150 | returnType.compileCheck( 151 | value = returnStructure.notNullRawType in ReturnTypes.returnTypes, 152 | lazyErrorMessage = lazyErrorMessage 153 | ) 154 | return returnStructure 155 | } 156 | 157 | override fun defaultHandler(node: KSNode, data: Unit): VisitorResult? = null 158 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/VisitorResult.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.structure.ClassStructure 4 | 5 | /** 6 | * Visitor 结果 7 | */ 8 | internal data class VisitorResult( 9 | val classStructure: ClassStructure, 10 | ) -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/ApiModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.isHttpOrHttps 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.RequestMethod 8 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ApiModel 9 | import com.google.devtools.ksp.symbol.KSAnnotation 10 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 11 | 12 | /** 13 | * 项目名称:ktorfitx 14 | * 15 | * 作者昵称:li-jia-wei 16 | * 17 | * 创建日期:2024/7/3 14:17 18 | * 19 | * 文件介绍:ApiModelResolver 20 | */ 21 | internal object ApiModelResolver { 22 | 23 | private val urlRegex = "^\\S*[a-zA-Z0-9]+\\S*$".toRegex() 24 | 25 | fun KSFunctionDeclaration.resolve(): ApiModel { 26 | val annotations = RequestMethod.entries.mapNotNull { 27 | getKSAnnotationByType(it.annotation) 28 | } 29 | val funName = this.simpleName.asString() 30 | this.compileCheck(annotations.isNotEmpty()) { 31 | val requestMethods = RequestMethod.entries.joinToString { "@${it.annotation.simpleName!!}" } 32 | "$funName 方法缺少请求类型,请使用以下请求类型类型:$requestMethods" 33 | } 34 | this.compileCheck(annotations.size == 1) { 35 | val useAnnotations = annotations.joinToString() 36 | val useSize = annotations.size 37 | "$funName 方法只允许使用一种请求方法,而你使用了 $useAnnotations $useSize 个" 38 | } 39 | val annotation = annotations.first() 40 | val requestFunName = annotation.shortName.asString().lowercase() 41 | return ApiModel(requestFunName, annotation.getUrl(funName)) 42 | } 43 | 44 | private fun KSAnnotation.getUrl(funName: String): String { 45 | val url = this.getValue("url")!! 46 | if (url.isHttpOrHttps()) return url 47 | this.compileCheck(urlRegex.matches(url)) { 48 | "$funName 方法上 $this 注解的 url 参数格式错误" 49 | } 50 | return url 51 | } 52 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/BearerAuthModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.BearerAuth 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.BearerAuthModel 5 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 6 | import com.google.devtools.ksp.symbol.impl.hasAnnotation 7 | 8 | /** 9 | * 项目名称:ktorfitx 10 | * 11 | * 作者昵称:li-jia-wei 12 | * 13 | * 创建日期:2024/8/12 16:16 14 | * 15 | * 文件介绍:BearerAuthModelResolver 16 | */ 17 | internal object BearerAuthModelResolver { 18 | 19 | fun KSFunctionDeclaration.resolve(): BearerAuthModel? { 20 | val hasBearerAuth = hasAnnotation(BearerAuth::class.qualifiedName!!) 21 | return if (hasBearerAuth) BearerAuthModel else null 22 | } 23 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/BodyModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Body 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.BodyModel 6 | import com.google.devtools.ksp.KspExperimental 7 | import com.google.devtools.ksp.isAnnotationPresent 8 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 9 | 10 | /** 11 | * 项目名称:ktorfitx 12 | * 13 | * 作者昵称:li-jia-wei 14 | * 15 | * 创建日期:2024/7/3 13:25 16 | * 17 | * 文件介绍:BodyModelResolver 18 | */ 19 | internal object BodyModelResolver { 20 | 21 | @OptIn(KspExperimental::class) 22 | fun KSFunctionDeclaration.resolve(): BodyModel? { 23 | val valueParameters = this.parameters.filter { it.isAnnotationPresent(Body::class) } 24 | if (valueParameters.isEmpty()) return null 25 | this.compileCheck(valueParameters.size == 1) { 26 | "${this.simpleName.asString()} 方法不允许使用多个 @Body 注解" 27 | } 28 | val valueParameter = valueParameters.first() 29 | val varName = valueParameter.name!!.asString() 30 | val qualifiedName = valueParameter.type.resolve().declaration.qualifiedName?.asString() 31 | this.compileCheck(qualifiedName != null) { 32 | "${this.simpleName.asString()} 方法的参数列表中标记了 @Body 注解,但是未找到参数类型" 33 | } 34 | return BodyModel(varName, qualifiedName) 35 | } 36 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/ExceptionListenerResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.* 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.kotlinpoet.ReturnTypes 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ExceptionListenerModel 8 | import com.google.devtools.ksp.getClassDeclarationByName 9 | import com.google.devtools.ksp.processing.Resolver 10 | import com.google.devtools.ksp.symbol.ClassKind 11 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 12 | import com.google.devtools.ksp.symbol.Modifier 13 | import com.squareup.kotlinpoet.ClassName 14 | import com.squareup.kotlinpoet.ksp.toTypeName 15 | 16 | /** 17 | * 项目名称:ktorfitx 18 | * 19 | * 作者昵称:li-jia-wei 20 | * 21 | * 创建日期:2024/8/13 15:43 22 | * 23 | * 文件介绍:ExceptionListenerResolver 24 | */ 25 | internal object ExceptionListenerResolver { 26 | 27 | private val exceptionListenersClassName = ClassName.bestGuess(KtorfitxQualifiers.EXCEPTION_LISTENERS) 28 | 29 | fun KSFunctionDeclaration.resolves(resolver: Resolver): List { 30 | val annotation = getKSAnnotationByType(exceptionListenersClassName) ?: return emptyList() 31 | 32 | val listenerClassNames = mutableListOf() 33 | listenerClassNames += annotation.getClassName("listener")!! 34 | annotation.getClassNames("listeners")?.let { listenerClassNames += it } 35 | val exceptionListenerModels = listenerClassNames.map { 36 | annotation.compileCheck(it.canonicalName != KtorfitxQualifiers.EXCEPTION_LISTENER) { 37 | val funName = this.simpleName.asString() 38 | "$funName 方法上的 @ExceptionListeners 注解中不能使用 ExceptionListener::class,请实现它并且必须是非 private 访问权限的 object 类型的" 39 | } 40 | val classDeclaration = resolver.getClassDeclarationByName(it.canonicalName)!! 41 | val classKind = classDeclaration.classKind 42 | classDeclaration.compileCheck(classKind == ClassKind.OBJECT) { 43 | "${it.simpleName} 类不允许使用 ${classKind.code} 类型,请使用 object 类型" 44 | } 45 | classDeclaration.compileCheck(!classDeclaration.modifiers.contains(Modifier.PRIVATE)) { 46 | "${it.simpleName} 类不允许使用 private 访问权限" 47 | } 48 | val typeArguments = classDeclaration.superTypes.first().element!!.typeArguments 49 | val exceptionTypeName = typeArguments[0].toTypeName() 50 | val returnTypeName = typeArguments[1].toTypeName() 51 | val funReturnTypeName = this.returnType!!.toTypeName().copy(nullable = false) 52 | annotation.compileCheck(returnTypeName == ReturnTypes.unitClassName || returnTypeName == funReturnTypeName) { 53 | val funName = this.simpleName.asString() 54 | val returnSimpleNames = if (returnTypeName.simpleName != funReturnTypeName.simpleName) { 55 | returnTypeName.simpleName to funReturnTypeName.simpleName 56 | } else { 57 | returnTypeName.rawType.canonicalName to funReturnTypeName.rawType.canonicalName 58 | } 59 | "$funName 方法上的 @ExceptionListeners 注解中的 ${it.simpleName} 监听器的返回类型 ${returnSimpleNames.first} 与方法中的返回类型 ${returnSimpleNames.second} 不一致" 60 | } 61 | ExceptionListenerModel(it, exceptionTypeName, returnTypeName) 62 | } 63 | exceptionListenerModels.groupBy { it.listenerClassName }.forEach { (className, models) -> 64 | annotation.compileCheck(models.size == 1) { 65 | val funName = this.simpleName.asString() 66 | "$funName 方法上的 @ExceptionListeners 中存在相同的监听器 ${className.simpleName}" 67 | } 68 | } 69 | exceptionListenerModels.groupBy { it.exceptionTypeName }.forEach { (className, models) -> 70 | annotation.compileCheck(models.size == 1) { 71 | val funName = this.simpleName.asString() 72 | val listeners = models.joinToString { it.listenerClassName.simpleName } 73 | "$funName 方法上的 @ExceptionListeners 中的监听器 $listeners 中同时处理了相同的异常 ${className.simpleName}" 74 | } 75 | } 76 | return exceptionListenerModels 77 | } 78 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/FormModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Form 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.FormModel 7 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/8/17 18:53 15 | * 16 | * 文件介绍:FormModelResolver 17 | */ 18 | internal object FormModelResolver { 19 | 20 | fun KSFunctionDeclaration.resolves(): List { 21 | return this.parameters.mapNotNull { valueParameter -> 22 | val annotation = valueParameter.getKSAnnotationByType(Form::class) ?: return@mapNotNull null 23 | val varName = valueParameter.name!!.asString() 24 | var name = annotation.getValue(Form::name) 25 | if (name.isNullOrBlank()) { 26 | name = varName 27 | } 28 | FormModel(name, varName) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/HeaderModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Header 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.HeaderModel 7 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/7/9 22:12 15 | * 16 | * 文件介绍:HeaderModelResolver 17 | */ 18 | internal object HeaderModelResolver { 19 | 20 | private val regex = "([a-z])([A-Z])".toRegex() 21 | 22 | fun KSFunctionDeclaration.resolves(): List { 23 | return this.parameters.mapNotNull { valueParameter -> 24 | val annotation = valueParameter.getKSAnnotationByType(Header::class) ?: return@mapNotNull null 25 | var name = annotation.getValue(Header::name) 26 | val varName = valueParameter.name!!.asString() 27 | if (name.isNullOrBlank()) { 28 | name = varName.replace(regex) { 29 | "${it.groupValues[1]}-${it.groupValues[2]}" 30 | }.replaceFirstChar { it.uppercase() } 31 | } 32 | HeaderModel(name, varName) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/HeadersModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Headers 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValues 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.HeadersModel 7 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/7/2 22:22 15 | * 16 | * 文件介绍:HeadersModelResolver 17 | */ 18 | internal object HeadersModelResolver { 19 | 20 | private val headersRegex = "^([^:=]+)[:=]([^:=]+)$".toRegex() 21 | 22 | fun KSFunctionDeclaration.resolve(): HeadersModel? { 23 | val annotation = getKSAnnotationByType(Headers::class) ?: return null 24 | val headers = annotation.getValues(Headers::headers) ?: return null 25 | return headers.associate { 26 | val (name, value) = headersRegex.matchEntire(it)?.destructured 27 | ?: error("${qualifiedName!!.asString()} 方法的 @Headers 格式错误") 28 | name.trim() to value.trim() 29 | }.let { HeadersModel(it) } 30 | } 31 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/MockModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.* 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.MockModel 7 | import com.google.devtools.ksp.symbol.ClassKind 8 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 9 | import com.google.devtools.ksp.symbol.Modifier 10 | import com.squareup.kotlinpoet.ClassName 11 | 12 | /** 13 | * 项目名称:ktorfitx 14 | * 15 | * 作者昵称:li-jia-wei 16 | * 17 | * 创建日期:2024/7/2 22:50 18 | * 19 | * 文件介绍:MockModelResolver 20 | */ 21 | internal object MockModelResolver { 22 | 23 | private val mockClassName by lazy { 24 | ClassName.bestGuess(KtorfitxQualifiers.MOCK) 25 | } 26 | private val mockProviderClassName by lazy { 27 | ClassName.bestGuess(KtorfitxQualifiers.MOCK_PROVIDER) 28 | } 29 | private val statusSuccessClassName by lazy { 30 | ClassName(KtorfitxQualifiers.PACKAGE_API_MOCK, "MockStatus", "SUCCESS") 31 | } 32 | 33 | fun KSFunctionDeclaration.resolve(): MockModel? { 34 | val annotation = getKSAnnotationByType(mockClassName) ?: return null 35 | val className = annotation.getClassName("provider")!! 36 | val delayRange = annotation.getValues("delayRange") ?: arrayOf(200L) 37 | 38 | annotation.compileCheck(className != mockProviderClassName) { 39 | val funName = this.simpleName.asString() 40 | "$funName 方法上的 @Mock 注解的 provider 参数不允许使用 MockProvider::class" 41 | } 42 | val classDeclaration = annotation.getArgumentKSClassDeclaration("provider")!! 43 | val classKind = classDeclaration.classKind 44 | classDeclaration.compileCheck(classKind == ClassKind.OBJECT) { 45 | "${className.simpleName} 类不允许使用 ${classKind.code} 类型,请使用 object 类型" 46 | } 47 | classDeclaration.compileCheck(!classDeclaration.modifiers.contains(Modifier.PRIVATE)) { 48 | "${className.simpleName} 类不允许使用 private 访问权限" 49 | } 50 | annotation.compileCheck(delayRange.size == 1 || (delayRange.size == 2 && delayRange[0] <= delayRange[1])) { 51 | val funName = simpleName.asString() 52 | "$funName 方法上的 @Mock 注解的 delayRange 参数只允许1个固定延迟时长参数或2个范围随机延长参数,并且2个参数的情况下必须 delayRange[0] <= delayRange[1]" 53 | } 54 | val status = annotation.getClassName("status") ?: statusSuccessClassName 55 | return MockModel(className, status, delayRange) 56 | } 57 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/ModelResolvers.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.* 5 | import com.google.devtools.ksp.processing.Resolver 6 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 7 | 8 | /** 9 | * 项目名称:ktorfitx 10 | * 11 | * 作者昵称:li-jia-wei 12 | * 13 | * 创建日期:2024/7/6 9:42 14 | * 15 | * 文件介绍:ModelResolvers 16 | */ 17 | internal object ModelResolvers { 18 | 19 | /** 20 | * 获取所有 ParameterModel 21 | */ 22 | fun KSFunctionDeclaration.getAllParameterModel(): List { 23 | return with(ParameterModelResolver) { resolves() } 24 | } 25 | 26 | /** 27 | * 获取所有 ValueParameterModel 28 | */ 29 | fun KSFunctionDeclaration.getAllValueParameterModels(): List { 30 | val models = mutableListOf() 31 | models += with(BodyModelResolver) { resolve() } 32 | models += with(QueryModelResolver) { resolves() } 33 | models += with(FormModelResolver) { resolves() } 34 | models += with(PathModelResolver) { resolves() } 35 | models += with(HeaderModelResolver) { resolves() } 36 | val useBodyAndForm = arrayOf(BodyModel::class, FormModel::class) 37 | .all { kClass -> models.any { kClass.isInstance(it) } } 38 | this.compileCheck(!useBodyAndForm) { 39 | val funName = this.simpleName.asString() 40 | "$funName 方法不能同时使用 @Body 和 @Form 注解" 41 | } 42 | return models.filterNotNull() 43 | } 44 | 45 | /** 46 | * 获取所有 FunctionModel 47 | */ 48 | fun KSFunctionDeclaration.getAllFunctionModels(resolver: Resolver): List { 49 | val models = mutableListOf() 50 | models += with(ApiModelResolver) { resolve() } 51 | models += with(HeadersModelResolver) { resolve() } 52 | models += with(MockModelResolver) { resolve() } 53 | models += with(BearerAuthModelResolver) { resolve() } 54 | models += with(ExceptionListenerResolver) { resolves(resolver) } 55 | return models.filterNotNull() 56 | } 57 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/ParameterModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.ksp.check.compileCheck 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.constants.KtorfitxQualifiers 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.isLowerCamelCase 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.lowerCamelCase 7 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.ParameterModel 8 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 9 | import com.squareup.kotlinpoet.ksp.toTypeName 10 | 11 | /** 12 | * 项目名称:ktorfitx 13 | * 14 | * 作者昵称:li-jia-wei 15 | * 16 | * 创建日期:2024/8/17 22:47 17 | * 18 | * 文件介绍:ParameterModelResolver 19 | */ 20 | internal object ParameterModelResolver { 21 | 22 | private val annotationQualifiedNames = arrayOf( 23 | KtorfitxQualifiers.BODY, 24 | KtorfitxQualifiers.FORM, 25 | KtorfitxQualifiers.HEADER, 26 | KtorfitxQualifiers.PATH, 27 | KtorfitxQualifiers.QUERY 28 | ) 29 | 30 | fun KSFunctionDeclaration.resolves(): List { 31 | return this.parameters.map { valueParameter -> 32 | val varName = valueParameter.name!!.asString() 33 | val annotationCount = valueParameter.annotations.toList() 34 | .map { it.annotationType.resolve().declaration.qualifiedName?.asString() } 35 | .count { it in annotationQualifiedNames } 36 | this.compileCheck(annotationCount > 0) { 37 | val funName = this.simpleName.asString() 38 | "$funName 方法上 $varName 参数未使用任何功能注解" 39 | } 40 | this.compileCheck(annotationCount == 1) { 41 | val funName = this.simpleName.asString() 42 | val useAnnotations = this.annotations.joinToString() 43 | "$funName 方法上 $varName 参数不允许同时使用 $useAnnotations 多个注解" 44 | } 45 | this.compileCheck(varName.isLowerCamelCase()) { 46 | val funName = this.simpleName.asString() 47 | val varNameSuggestion = varName.lowerCamelCase() 48 | "$funName 方法上 $varName 参数不符合小驼峰命名规则,建议修改为 $varNameSuggestion" 49 | } 50 | val typeName = valueParameter.type.toTypeName() 51 | ParameterModel(varName, typeName) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/PathModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Path 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.PathModel 7 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/8/17 18:53 15 | * 16 | * 文件介绍:PathModelResolver 17 | */ 18 | internal object PathModelResolver { 19 | 20 | fun KSFunctionDeclaration.resolves(): List { 21 | return this.parameters.mapNotNull { valueParameter -> 22 | val annotation = valueParameter.getKSAnnotationByType(Path::class) ?: return@mapNotNull null 23 | val varName = valueParameter.name!!.asString() 24 | val name = annotation.getValue(Path::name) ?: varName 25 | PathModel(name, varName, valueParameter) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/kotlin/cn/vividcode/multiplatform/ktorfitx/ksp/visitor/resolver/QueryModelResolver.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.ksp.visitor.resolver 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.Query 4 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getKSAnnotationByType 5 | import cn.vividcode.multiplatform.ktorfitx.ksp.expends.getValue 6 | import cn.vividcode.multiplatform.ktorfitx.ksp.model.model.QueryModel 7 | import com.google.devtools.ksp.symbol.KSFunctionDeclaration 8 | 9 | /** 10 | * 项目名称:ktorfitx 11 | * 12 | * 作者昵称:li-jia-wei 13 | * 14 | * 创建日期:2024/8/17 18:53 15 | * 16 | * 文件介绍:QueryModelResolver 17 | */ 18 | internal object QueryModelResolver { 19 | 20 | fun KSFunctionDeclaration.resolves(): List { 21 | return this.parameters.mapNotNull { valueParameter -> 22 | val annotation = valueParameter.getKSAnnotationByType(Query::class) ?: return@mapNotNull null 23 | var name = annotation.getValue(Query::name) 24 | val varName = valueParameter.name!!.asString() 25 | if (name.isNullOrBlank()) { 26 | name = varName 27 | } 28 | QueryModel(name, varName) 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ktorfitx-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | cn.vividcode.multiplatform.ktorfitx.ksp.KtorfitSymbolProcessorProvider -------------------------------------------------------------------------------- /ktorfitx-sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 6 | 7 | plugins { 8 | alias(libs.plugins.kotlin.multiplatform) 9 | alias(libs.plugins.android.application) 10 | alias(libs.plugins.jetbrains.compose) 11 | alias(libs.plugins.compose.compiler) 12 | alias(libs.plugins.ksp) 13 | alias(libs.plugins.kotlin.serialization) 14 | } 15 | 16 | val ktorfitxSampleVersion = property("ktorfitx.sample.version").toString() 17 | 18 | kotlin { 19 | androidTarget { 20 | compilerOptions { 21 | jvmTarget.set(JvmTarget.JVM_21) 22 | } 23 | } 24 | 25 | jvm("desktop") { 26 | compilerOptions { 27 | jvmTarget.set(JvmTarget.JVM_21) 28 | } 29 | } 30 | 31 | listOf( 32 | iosX64(), 33 | iosArm64(), 34 | iosSimulatorArm64() 35 | ).forEach { iosTarget -> 36 | iosTarget.binaries.framework { 37 | baseName = "SampleApp" 38 | isStatic = true 39 | } 40 | } 41 | 42 | js { 43 | outputModuleName = "sampleApp" 44 | browser { 45 | commonWebpackConfig { 46 | outputFileName = "sampleApp.js" 47 | } 48 | } 49 | binaries.executable() 50 | useEsModules() 51 | } 52 | 53 | @OptIn(ExperimentalWasmDsl::class) 54 | wasmJs { 55 | outputModuleName = "sampleApp" 56 | browser { 57 | val rootDirPath = project.rootDir.path 58 | val projectDirPath = project.projectDir.path 59 | commonWebpackConfig { 60 | outputFileName = "sampleApp.js" 61 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply { 62 | static = (static ?: mutableListOf()).apply { 63 | add(rootDirPath) 64 | add(projectDirPath) 65 | } 66 | } 67 | } 68 | } 69 | binaries.executable() 70 | } 71 | 72 | sourceSets { 73 | commonMain { 74 | dependencies { 75 | implementation(projects.ktorfitxApi) 76 | implementation(projects.ktorfitxAnnotation) 77 | implementation(libs.ktor.client.core) 78 | implementation(libs.ktor.client.logging) 79 | implementation(libs.ktor.client.cio) 80 | implementation(libs.ktor.client.serialization) 81 | implementation(libs.ktor.client.content.negotiation) 82 | implementation(libs.ktor.serialization.kotlinx.json) 83 | implementation(compose.runtime) 84 | implementation(compose.foundation) 85 | implementation(compose.material3) 86 | implementation(compose.ui) 87 | implementation(compose.components.resources) 88 | implementation(compose.components.uiToolingPreview) 89 | } 90 | kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") 91 | } 92 | androidMain { 93 | dependencies { 94 | implementation(compose.preview) 95 | implementation(libs.androidx.activity.compose) 96 | } 97 | } 98 | val desktopMain by getting 99 | desktopMain.dependencies { 100 | implementation(compose.desktop.currentOs) 101 | } 102 | } 103 | } 104 | 105 | dependencies { 106 | kspCommonMainMetadata(projects.ktorfitxKsp) 107 | } 108 | 109 | tasks.withType>().all { 110 | "kspCommonMainKotlinMetadata".also { 111 | if (name != it) dependsOn(it) 112 | } 113 | } 114 | 115 | android { 116 | namespace = "cn.vividcode.multiplatform.config.sample" 117 | compileSdk = libs.versions.android.compileSdk.get().toInt() 118 | 119 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 120 | sourceSets["main"].res.srcDirs("src/androidMain/res") 121 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 122 | 123 | defaultConfig { 124 | applicationId = "cn.vividcode.multiplatform.config.sample" 125 | minSdk = libs.versions.android.minSdk.get().toInt() 126 | targetSdk = libs.versions.android.targetSdk.get().toInt() 127 | versionCode = 2 128 | versionName = ktorfitxSampleVersion 129 | } 130 | packaging { 131 | resources { 132 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 133 | } 134 | } 135 | buildTypes { 136 | getByName("release") { 137 | isMinifyEnabled = false 138 | } 139 | } 140 | compileOptions { 141 | sourceCompatibility = JavaVersion.VERSION_21 142 | targetCompatibility = JavaVersion.VERSION_21 143 | } 144 | buildFeatures { 145 | compose = true 146 | } 147 | dependencies { 148 | debugImplementation(compose.uiTooling) 149 | } 150 | } 151 | 152 | compose.desktop { 153 | application { 154 | mainClass = "cn.vividcode.multiplatform.ktorfitx.sample.MainKt" 155 | 156 | nativeDistributions { 157 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Exe) 158 | packageName = "cn.vividcode.multiplatform.ktorfitx.sample" 159 | packageVersion = ktorfitxSampleVersion 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.tooling.preview.Preview 8 | 9 | class MainActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | setContent { 14 | App() 15 | } 16 | } 17 | } 18 | 19 | @Preview 20 | @Composable 21 | fun AppAndroidPreview() { 22 | App() 23 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * MainApplication 7 | */ 8 | class MainApplication : Application() -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kotlin-flex/ktorfitx/bedcacec8b56162270ec3e649b464d8511d25595/ktorfitx-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /ktorfitx-sample/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ktorfitx-sample 3 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/App.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.horizontalScroll 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.unit.dp 15 | import cn.vividcode.multiplatform.ktorfitx.sample.http.api.impl.testApi 16 | import cn.vividcode.multiplatform.ktorfitx.sample.http.testKtorfit 17 | import kotlinx.coroutines.launch 18 | import kotlinx.serialization.encodeToString 19 | import kotlinx.serialization.json.Json 20 | 21 | private val json = Json { prettyPrint = true } 22 | 23 | @Composable 24 | fun App() { 25 | MaterialTheme { 26 | val coroutineScope = rememberCoroutineScope() 27 | Scaffold( 28 | modifier = Modifier 29 | .fillMaxSize() 30 | ) { paddingValues -> 31 | Column( 32 | modifier = Modifier 33 | .fillMaxSize() 34 | .padding(paddingValues) 35 | .padding(top = 16.dp), 36 | horizontalAlignment = Alignment.CenterHorizontally, 37 | verticalArrangement = Arrangement.Center 38 | ) { 39 | Text( 40 | text = "聚合数据:头条新闻(测试)", 41 | style = MaterialTheme.typography.titleLarge 42 | ) 43 | Text( 44 | text = "注:接口数据由聚合数据提供", 45 | style = MaterialTheme.typography.titleSmall, 46 | color = MaterialTheme.colorScheme.error 47 | ) 48 | var key by remember { mutableStateOf("f28b487e8ed22222fcfb7cf38df2d611") } 49 | OutlinedTextField( 50 | value = key, 51 | onValueChange = { key = it }, 52 | placeholder = { 53 | Text( 54 | text = "f28b487e8ed22222fcfb7cf38df2d611", 55 | color = MaterialTheme.colorScheme.outline 56 | ) 57 | }, 58 | label = { Text("接口Key") }, 59 | modifier = Modifier 60 | .width(380.dp) 61 | .padding( 62 | top = 16.dp, 63 | bottom = 8.dp 64 | ) 65 | ) 66 | var type by remember { mutableStateOf("top") } 67 | var isFilter by remember { mutableStateOf(0) } 68 | Row( 69 | modifier = Modifier 70 | .width(380.dp) 71 | .padding(vertical = 8.dp) 72 | ) { 73 | TypeDropdown( 74 | value = type, 75 | onValueChange = { type = it }, 76 | modifier = Modifier 77 | .weight(1f) 78 | ) 79 | Spacer(modifier = Modifier.width(16.dp)) 80 | FilterDetail( 81 | value = isFilter, 82 | onValueChange = { isFilter = it }, 83 | modifier = Modifier 84 | .weight(1f) 85 | ) 86 | } 87 | var page by remember { 88 | mutableStateOf("1", object : SnapshotMutationPolicy { 89 | private val regex = "^(?:[1-9]|[1-4][0-9]|50)?\$".toRegex() 90 | override fun equivalent(a: String, b: String): Boolean { 91 | return !regex.matches(b) 92 | } 93 | }) 94 | } 95 | var pageSize by remember { 96 | mutableStateOf("10", object : SnapshotMutationPolicy { 97 | private val regex = "^(?:[1-9]|[1-2][0-9]|30)?\$".toRegex() 98 | override fun equivalent(a: String, b: String): Boolean { 99 | return !regex.matches(b) 100 | } 101 | }) 102 | } 103 | Row( 104 | modifier = Modifier 105 | .width(380.dp) 106 | .padding(vertical = 8.dp), 107 | ) { 108 | OutlinedTextField( 109 | value = page, 110 | onValueChange = { page = it }, 111 | modifier = Modifier.weight(1f), 112 | placeholder = { Text("默认:1,最大:50") }, 113 | label = { Text("当前页数") }, 114 | singleLine = true, 115 | ) 116 | Spacer(modifier = Modifier.width(16.dp)) 117 | OutlinedTextField( 118 | value = pageSize, 119 | onValueChange = { pageSize = it }, 120 | modifier = Modifier.weight(1f), 121 | placeholder = { Text("默认:30,最大:30") }, 122 | label = { Text("每页条数") }, 123 | singleLine = true, 124 | ) 125 | } 126 | var text: String? by remember { mutableStateOf(null) } 127 | Button( 128 | modifier = Modifier 129 | .padding(vertical = 8.dp) 130 | .width(380.dp) 131 | .clip(RoundedCornerShape(4.dp)), 132 | onClick = { 133 | coroutineScope.launch { 134 | val correctKey = key.ifBlank { "f28b487e8ed22222fcfb7cf38df2d611" } 135 | val result = testKtorfit.testApi.headlineNews(correctKey, type, page, pageSize, isFilter) 136 | if (result == null) { 137 | text = "" 138 | return@launch 139 | } 140 | val jsonElement = json.parseToJsonElement(result) 141 | text = json.encodeToString(jsonElement) 142 | } 143 | } 144 | ) { 145 | Text("点击查询") 146 | } 147 | Spacer(modifier = Modifier.height(8.dp)) 148 | val horizontalScrollState = rememberScrollState() 149 | val verticalScrollState = rememberScrollState() 150 | Text( 151 | text = text ?: "", 152 | modifier = Modifier 153 | .padding(8.dp) 154 | .fillMaxWidth() 155 | .weight(1f) 156 | .background( 157 | color = MaterialTheme.colorScheme.surfaceContainer, 158 | shape = RoundedCornerShape(8.dp) 159 | ) 160 | .padding(vertical = 8.dp) 161 | .horizontalScroll(horizontalScrollState) 162 | .verticalScroll(verticalScrollState) 163 | .padding( 164 | horizontal = 16.dp, 165 | vertical = 8.dp 166 | ), 167 | color = MaterialTheme.colorScheme.onSurface 168 | ) 169 | } 170 | } 171 | } 172 | } 173 | 174 | @OptIn(ExperimentalMaterial3Api::class) 175 | @Composable 176 | private fun TypeDropdown( 177 | value: String, 178 | onValueChange: (String) -> Unit, 179 | modifier: Modifier = Modifier, 180 | ) { 181 | var typeExpanded by remember { mutableStateOf(false) } 182 | val options = mapOf( 183 | "top" to "推荐", 184 | "guonei" to "国内", 185 | "guoji" to "国际", 186 | "yule" to "娱乐", 187 | "tiyu" to "体育", 188 | "junshi" to "军事", 189 | "keji" to "科技", 190 | "caijing" to "财经", 191 | "youxi" to "游戏", 192 | "qiche" to "汽车", 193 | "jiankang" to "健康" 194 | ) 195 | ExposedDropdownMenuBox( 196 | expanded = typeExpanded, 197 | onExpandedChange = { typeExpanded = !typeExpanded }, 198 | modifier = modifier 199 | ) { 200 | OutlinedTextField( 201 | value = options[value]!!, 202 | onValueChange = { }, 203 | modifier = modifier 204 | .menuAnchor(MenuAnchorType.SecondaryEditable), 205 | readOnly = true, 206 | label = { Text("类型") }, 207 | colors = ExposedDropdownMenuDefaults.textFieldColors(), 208 | trailingIcon = { 209 | ExposedDropdownMenuDefaults.TrailingIcon( 210 | expanded = typeExpanded 211 | ) 212 | } 213 | ) 214 | ExposedDropdownMenu( 215 | expanded = typeExpanded, 216 | onDismissRequest = { typeExpanded = false }, 217 | modifier = modifier 218 | ) { 219 | options.forEach { (key, value) -> 220 | DropdownMenuItem( 221 | text = { Text(value) }, 222 | onClick = { 223 | onValueChange(key) 224 | typeExpanded = false 225 | } 226 | ) 227 | } 228 | } 229 | } 230 | } 231 | 232 | @OptIn(ExperimentalMaterial3Api::class) 233 | @Composable 234 | private fun FilterDetail( 235 | value: Int, 236 | onValueChange: (Int) -> Unit, 237 | modifier: Modifier = Modifier, 238 | ) { 239 | var typeExpanded by remember { mutableStateOf(false) } 240 | val options = listOf("否", "是") 241 | ExposedDropdownMenuBox( 242 | expanded = typeExpanded, 243 | onExpandedChange = { typeExpanded = !typeExpanded }, 244 | modifier = modifier 245 | ) { 246 | OutlinedTextField( 247 | value = options[value], 248 | onValueChange = { }, 249 | modifier = modifier 250 | .menuAnchor(MenuAnchorType.SecondaryEditable), 251 | readOnly = true, 252 | label = { Text("必须包含详情信息") }, 253 | colors = ExposedDropdownMenuDefaults.textFieldColors(), 254 | trailingIcon = { 255 | ExposedDropdownMenuDefaults.TrailingIcon( 256 | expanded = typeExpanded 257 | ) 258 | } 259 | ) 260 | ExposedDropdownMenu( 261 | expanded = typeExpanded, 262 | onDismissRequest = { typeExpanded = false }, 263 | modifier = modifier 264 | ) { 265 | options.forEachIndexed { index, s -> 266 | DropdownMenuItem( 267 | text = { Text(s) }, 268 | onClick = { 269 | onValueChange(index) 270 | typeExpanded = false 271 | } 272 | ) 273 | } 274 | } 275 | } 276 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/TestKtorfit.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.ktorfit 4 | import cn.vividcode.multiplatform.ktorfitx.api.scope.ApiScope 5 | import io.ktor.client.engine.cio.* 6 | import io.ktor.client.plugins.contentnegotiation.* 7 | import io.ktor.serialization.kotlinx.json.* 8 | import kotlinx.serialization.json.Json 9 | 10 | val testKtorfit = ktorfit(TestApiScope) { 11 | token { "" } 12 | baseUrl = "http://localhost:8080/api/" 13 | httpClient(CIO) { 14 | engine { 15 | requestTimeout = 10_000L 16 | maxConnectionsCount = 200 17 | } 18 | install(ContentNegotiation) { 19 | json( 20 | Json { 21 | prettyPrint = true 22 | ignoreUnknownKeys = true 23 | } 24 | ) 25 | } 26 | } 27 | } 28 | 29 | object TestApiScope : ApiScope -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/TestRequest2.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TestRequest2( 7 | val param1: String 8 | ) -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/api/Test.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.api 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Test( 7 | val test: String 8 | ) -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/api/Test2Api.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.api 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.* 4 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockProvider 5 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus 6 | import cn.vividcode.multiplatform.ktorfitx.sample.http.TestApiScope 7 | 8 | @Api(url = "/test2", apiScope = TestApiScope::class) 9 | interface Test2Api { 10 | 11 | @POST(url = "/{path}") 12 | suspend fun test( 13 | @Path path: String, 14 | ): String 15 | 16 | @GET(url = "/mockTest") 17 | @Mock(provider = TestMockProvider::class) 18 | suspend fun mockTest(): String 19 | } 20 | 21 | object TestMockProvider : MockProvider { 22 | override fun provide(status: MockStatus): String { 23 | return "" 24 | } 25 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/api/TestApi.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.api 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.annotation.* 4 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus 5 | import cn.vividcode.multiplatform.ktorfitx.api.model.ResultBody 6 | import cn.vividcode.multiplatform.ktorfitx.sample.http.TestApiScope 7 | import cn.vividcode.multiplatform.ktorfitx.sample.http.TestRequest2 8 | import cn.vividcode.multiplatform.ktorfitx.sample.http.listener.TestResultBodyExceptionListener 9 | import cn.vividcode.multiplatform.ktorfitx.sample.http.listener.TestUnitExceptionListener 10 | import cn.vividcode.multiplatform.ktorfitx.sample.http.mock.ResultBodyMockProvider 11 | import cn.vividcode.multiplatform.ktorfitx.sample.http.mock.StringMockProvider 12 | import kotlinx.serialization.Serializable 13 | 14 | /** 15 | * 项目名称:ktorfitx 16 | * 17 | * 作者昵称:li-jia-wei 18 | * 19 | * 创建日期:2024/8/4 23:09 20 | * 21 | * 文件介绍:TestApi 22 | */ 23 | @Api(url = "user", apiScope = TestApiScope::class) 24 | interface TestApi { 25 | 26 | @GET(url = "test01") 27 | @ExceptionListeners(TestResultBodyExceptionListener::class) 28 | suspend fun test01(): String 29 | 30 | @POST(url = "/test02") 31 | @Headers("Content-Type: application/json") 32 | @ExceptionListeners(TestUnitExceptionListener::class) 33 | suspend fun test02( 34 | @Body testRequest: TestRequest2, 35 | @Header testHeader: String, 36 | ): ResultBody 37 | 38 | @BearerAuth 39 | @PUT(url = "/test03") 40 | suspend fun test03( 41 | @Form form1: String, 42 | ): ResultBody 43 | 44 | @BearerAuth 45 | @DELETE(url = "/test04/{deleteId}") 46 | suspend fun test04( 47 | @Path deleteId: Int, 48 | ): ResultBody 49 | 50 | @PATCH(url = "/test05") 51 | suspend fun test05( 52 | @Form form1: String, 53 | ): ByteArray 54 | 55 | @BearerAuth 56 | @OPTIONS(url = "/{name}/test06") 57 | suspend fun test06( 58 | @Path name: String, 59 | ): ByteArray 60 | 61 | @HEAD(url = "/test07") 62 | suspend fun test07() 63 | 64 | @ExceptionListeners(TestUnitExceptionListener::class) 65 | @GET(url = "/test08") 66 | suspend fun test08(): ByteArray? 67 | 68 | @GET(url = "/test09") 69 | suspend fun test09(): String 70 | 71 | @POST(url = "/test10") 72 | suspend fun test10(): String? 73 | 74 | @BearerAuth 75 | @Mock(ResultBodyMockProvider::class, MockStatus.SUCCESS, delayRange = [1000, 2000]) 76 | @GET(url = "/testMock01") 77 | suspend fun testMock01( 78 | @Query param1: String, 79 | @Query param2: String, 80 | ): ResultBody 81 | 82 | @Mock(ResultBodyMockProvider::class, MockStatus.EXCEPTION) 83 | @ExceptionListeners(TestUnitExceptionListener::class) 84 | @POST(url = "/testMock02") 85 | suspend fun testMock02( 86 | @Body request: TestRequest, 87 | ): ResultBody 88 | 89 | @Mock(ResultBodyMockProvider::class) 90 | @PUT(url = "/testMock03") 91 | suspend fun testMock03( 92 | @Form form1: String, 93 | ): ResultBody 94 | 95 | @Mock(ResultBodyMockProvider::class) 96 | @DELETE(url = "/testMock04/{deleteId}") 97 | suspend fun testMock04( 98 | @Path deleteId: Int, 99 | ): ResultBody 100 | 101 | @Mock(StringMockProvider::class) 102 | @GET(url = "/testMock05") 103 | suspend fun testMock05(): String 104 | 105 | @Mock(StringMockProvider::class) 106 | @GET(url = "/testMock06") 107 | suspend fun testMock06(): String? 108 | 109 | @GET(url = "http://v.juhe.cn/toutiao/index") 110 | suspend fun headlineNews( 111 | @Query("key") key: String, 112 | @Query("type") type: String, 113 | @Query("page") page: String, 114 | @Query("page_size") pageSize: String, 115 | @Query("is_filter") isFilter: Int, 116 | ): String? 117 | 118 | @GET(url = "/test") 119 | suspend fun test(): ResultBody 120 | } 121 | 122 | @Serializable 123 | data class TestRequest( 124 | val param1: String, 125 | val param2: String, 126 | ) 127 | 128 | @Serializable 129 | data class TestResponse( 130 | val param1: String, 131 | ) -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/listener/TestExceptionListener.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.listener 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.exception.ExceptionListener 4 | import kotlin.reflect.KFunction 5 | 6 | /** 7 | * TestUnitExceptionListener 8 | */ 9 | data object TestUnitExceptionListener : ExceptionListener { 10 | 11 | override fun KFunction<*>.onExceptionListener(e: TestException) { 12 | 13 | } 14 | } 15 | 16 | /** 17 | * TestResultBodyExceptionListener 18 | */ 19 | object TestResultBodyExceptionListener : ExceptionListener { 20 | 21 | override fun KFunction<*>.onExceptionListener(e: Exception): String { 22 | return e.toString() 23 | } 24 | } 25 | 26 | /** 27 | * TestException 28 | */ 29 | class TestException : Exception("TestExceptionListener 异常测试") -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/mock/ResultBodyMockProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.mock 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockProvider 4 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus 5 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus.* 6 | import cn.vividcode.multiplatform.ktorfitx.api.model.ResultBody 7 | import cn.vividcode.multiplatform.ktorfitx.sample.http.api.TestResponse 8 | import cn.vividcode.multiplatform.ktorfitx.sample.http.listener.TestException 9 | 10 | /** 11 | * 项目名称:ktorfitx 12 | * 13 | * 作者昵称:li-jia-wei 14 | * 15 | * 创建日期:2024/8/12 04:04 16 | * 17 | * 文件介绍:TestMockProvider 18 | */ 19 | data object ResultBodyMockProvider : MockProvider> { 20 | 21 | override fun provide(status: MockStatus): ResultBody { 22 | return when (status) { 23 | SUCCESS -> ResultBody.success(TestResponse("测试Mock 参数一"), "测试Mock 操作成功") 24 | FAILURE -> ResultBody.failure(-1, "测试Mock 操作失败") 25 | EXCEPTION -> throw TestException() 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/commonMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/http/mock/StringMockProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample.http.mock 2 | 3 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockProvider 4 | import cn.vividcode.multiplatform.ktorfitx.api.mock.MockStatus 5 | 6 | /** 7 | * 项目名称:ktorfitx 8 | * 9 | * 作者昵称:li-jia-wei 10 | * 11 | * 创建日期:2024/9/9 18:08 12 | * 13 | * 文件介绍:StringMockProvider 14 | */ 15 | object StringMockProvider : MockProvider { 16 | 17 | override fun provide(status: MockStatus): String { 18 | return status.toString() 19 | } 20 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/desktopMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/main.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import androidx.compose.ui.Alignment 4 | import androidx.compose.ui.unit.DpSize 5 | import androidx.compose.ui.unit.dp 6 | import androidx.compose.ui.window.Window 7 | import androidx.compose.ui.window.WindowPosition 8 | import androidx.compose.ui.window.application 9 | import androidx.compose.ui.window.rememberWindowState 10 | 11 | fun main() = application { 12 | Window( 13 | onCloseRequest = ::exitApplication, 14 | title = "ktorfitx-sample", 15 | state = rememberWindowState( 16 | position = WindowPosition.Aligned(Alignment.Center), 17 | size = DpSize(460.dp, 750.dp) 18 | ), 19 | resizable = false 20 | ) { 21 | App() 22 | } 23 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import cn.vividcode.multiplatform.ktorfitx.sample.App 3 | 4 | @Suppress("unused", "FunctionName") 5 | fun MainViewController() = ComposeUIViewController { App() } -------------------------------------------------------------------------------- /ktorfitx-sample/src/jsMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/main.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.CanvasBasedWindow 5 | import androidx.compose.ui.window.ComposeViewport 6 | import kotlinx.browser.document 7 | import org.jetbrains.skiko.wasm.onWasmReady 8 | 9 | @OptIn(ExperimentalComposeUiApi::class) 10 | fun main() { 11 | onWasmReady { 12 | CanvasBasedWindow("KtorfitxSample") { 13 | App() 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ktorfitx-sample 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/jsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | min-width: 400px; 8 | min-height: 300px; 9 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/wasmJsMain/kotlin/cn/vividcode/multiplatform/ktorfitx/sample/main.kt: -------------------------------------------------------------------------------- 1 | package cn.vividcode.multiplatform.ktorfitx.sample 2 | 3 | import androidx.compose.ui.ExperimentalComposeUiApi 4 | import androidx.compose.ui.window.ComposeViewport 5 | import kotlinx.browser.document 6 | import org.jetbrains.compose.resources.configureWebResources 7 | 8 | @OptIn(ExperimentalComposeUiApi::class) 9 | fun main() { 10 | configureWebResources { 11 | resourcePathMapping { path -> "./$path" } 12 | } 13 | ComposeViewport(document.body!!) { 14 | App() 15 | } 16 | } -------------------------------------------------------------------------------- /ktorfitx-sample/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ktorfitx-sample 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ktorfitx-sample/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | min-width: 400px; 8 | min-height: 300px; 9 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ktorfitx" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | google() 8 | gradlePluginPortal() 9 | mavenCentral() 10 | } 11 | } 12 | plugins { 13 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 14 | } 15 | 16 | dependencyResolutionManagement { 17 | @Suppress("UnstableApiUsage") 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 22 | } 23 | } 24 | include("ktorfitx-api") 25 | include("ktorfitx-ksp") 26 | include("ktorfitx-annotation") 27 | include("ktorfitx-sample") --------------------------------------------------------------------------------