├── .editorconfig ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── renovate.json ├── fuel ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── fuel │ │ │ ├── FuelBuilder.kt │ │ │ ├── HttpLoaderFactory.kt │ │ │ ├── RequestConvertible.kt │ │ │ ├── HttpResponse.kt │ │ │ ├── HttpLoader.kt │ │ │ ├── Parameters.kt │ │ │ ├── Fuel.kt │ │ │ ├── Request.kt │ │ │ ├── FuelRouting.kt │ │ │ ├── Strings.kt │ │ │ ├── UriSyntaxException.kt │ │ │ ├── Fuels.kt │ │ │ └── UriCodec.kt │ ├── wasmJsMain │ │ └── kotlin │ │ │ └── fuel │ │ │ ├── FuelBuilder.wasmJs.kt │ │ │ ├── Object.kt │ │ │ ├── WasmHttpLoader.kt │ │ │ └── HttpUrlFetcher.kt │ ├── jvmTest │ │ └── kotlin │ │ │ └── fuel │ │ │ ├── HttpLoaderFactoryTest.kt │ │ │ ├── RoutingTest.kt │ │ │ ├── HttpLoaderBuilderTest.kt │ │ │ ├── StringsTest.kt │ │ │ └── HttpLoaderTest.kt │ ├── appleMain │ │ └── kotlin │ │ │ └── fuel │ │ │ ├── FuelBuilder.kt │ │ │ ├── NSURLUtils.kt │ │ │ ├── AppleHttpLoader.kt │ │ │ └── HttpUrlFetcher.kt │ ├── jvmMain │ │ └── kotlin │ │ │ └── fuel │ │ │ ├── FuelBuilder.kt │ │ │ ├── HttpUrlFetcher.kt │ │ │ ├── OkHttpUtils.kt │ │ │ └── JVMHttpLoader.kt │ └── commonTest │ │ └── kotlin │ │ └── fuel │ │ ├── ParametersTest.kt │ │ ├── RequestTest.kt │ │ └── UriCodecTests.kt └── build.gradle.kts ├── plugins ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── publication.gradle.kts ├── .gitignore ├── settings.gradle.kts ├── fuel-forge-jvm ├── src │ ├── main │ │ └── kotlin │ │ │ └── fuel │ │ │ └── forge │ │ │ └── ResponseExtension.kt │ └── test │ │ └── kotlin │ │ └── fuel │ │ └── forge │ │ └── FuelForgeTest.kt └── build.gradle.kts ├── samples ├── mockbin-native │ ├── build.gradle.kts │ └── src │ │ └── commonMain │ │ └── kotlin │ │ └── Main.kt └── httpbin-wasm │ ├── src │ └── wasmJsMain │ │ ├── kotlin │ │ └── Main.kt │ │ └── resources │ │ └── index.html │ └── build.gradle.kts ├── .github ├── workflows │ ├── pr.yml │ ├── release.yml │ ├── main.yml │ └── linter.yml └── dependabot.yml ├── gradle.properties ├── fuel-jackson-jvm ├── src │ ├── main │ │ └── kotlin │ │ │ └── fuel │ │ │ └── jackson │ │ │ └── ResponseExtension.kt │ └── test │ │ └── kotlin │ │ └── fuel │ │ └── jackson │ │ └── FuelJacksonTest.kt └── build.gradle.kts ├── fuel-kotlinx-serialization ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── fuel │ │ │ └── serialization │ │ │ └── ResponseExtensions.kt │ ├── jvmTest │ │ └── kotlin │ │ │ └── fuel │ │ │ └── serialization │ │ │ └── FuelKotlinxSerializationTest.kt │ └── appleTest │ │ └── kotlin │ │ └── fuel │ │ └── serialization │ │ └── FuelKotlinxSerializationTest.kt └── build.gradle.kts ├── fuel-moshi-jvm ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── fuel │ │ └── moshi │ │ └── ResponseExtension.kt │ └── test │ └── kotlin │ └── fuel │ └── moshi │ └── FuelMoshiTest.kt ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /.editorconfig: -------------------------------------------------------------------------------- 1 | [*/build/**/*] 2 | ktlint = disabled -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kittinunf/fuel/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:recommended"] 4 | } 5 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/FuelBuilder.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public expect class FuelBuilder() { 4 | public fun build(): HttpLoader 5 | } 6 | -------------------------------------------------------------------------------- /fuel/src/wasmJsMain/kotlin/fuel/FuelBuilder.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public actual class FuelBuilder actual constructor() { 4 | public actual fun build(): HttpLoader = WasmHttpLoader() 5 | } 6 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/HttpLoaderFactory.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public fun interface HttpLoaderFactory { 4 | /** 5 | * Return a new [HttpLoader] 6 | */ 7 | public fun newHttpLoader(): HttpLoader 8 | } 9 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/RequestConvertible.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | /** 4 | * Anything that is [RequestConvertible] can be used as [request] 5 | */ 6 | public interface RequestConvertible { 7 | public val request: Request 8 | } 9 | -------------------------------------------------------------------------------- /plugins/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | } 8 | 9 | java { 10 | sourceCompatibility = JavaVersion.VERSION_17 11 | targetCompatibility = JavaVersion.VERSION_17 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .gradle 3 | .idea 4 | bin 5 | gen 6 | *.iml 7 | .directory 8 | node_modules 9 | local.properties 10 | 11 | .DS_Store 12 | .externalNativeBuild 13 | .database 14 | 15 | Pods/ 16 | 17 | .project 18 | .settings 19 | .classpath 20 | .gradletasknamecache -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/HttpResponse.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.io.Source 4 | 5 | public class HttpResponse { 6 | public var statusCode: Int = -1 7 | public lateinit var source: Source 8 | public var headers: Map = emptyMap() 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Fuel-MPP" 2 | 3 | include(":fuel") 4 | include(":fuel-forge-jvm") 5 | include(":fuel-jackson-jvm") 6 | include(":fuel-kotlinx-serialization") 7 | include(":fuel-moshi-jvm") 8 | 9 | // include(":samples:httpbin-wasm") 10 | include(":samples:mockbin-native") 11 | 12 | pluginManagement { 13 | includeBuild("plugins") 14 | } 15 | -------------------------------------------------------------------------------- /fuel-forge-jvm/src/main/kotlin/fuel/forge/ResponseExtension.kt: -------------------------------------------------------------------------------- 1 | package fuel.forge 2 | 3 | import com.github.kittinunf.forge.Forge 4 | import com.github.kittinunf.forge.core.DeserializedResult 5 | import com.github.kittinunf.forge.core.JSON 6 | import fuel.HttpResponse 7 | import kotlinx.io.readString 8 | 9 | public fun HttpResponse.toForge(deserializer: JSON.() -> DeserializedResult): DeserializedResult = 10 | Forge.modelFromJson(source.readString(), deserializer) 11 | -------------------------------------------------------------------------------- /samples/mockbin-native/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | } 4 | 5 | kotlin { 6 | macosX64 { 7 | binaries { 8 | executable() 9 | } 10 | } 11 | macosArm64 { 12 | binaries { 13 | executable() 14 | } 15 | } 16 | 17 | sourceSets { 18 | commonMain { 19 | dependencies { 20 | implementation(project(":fuel")) 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/mockbin-native/src/commonMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import fuel.FuelBuilder 2 | import kotlinx.coroutines.runBlocking 3 | import kotlinx.io.readString 4 | 5 | fun main() = 6 | runBlocking { 7 | val fuel = FuelBuilder().build() 8 | val response = 9 | fuel.post(request = { 10 | url = "http://mockbin.com/request?foo=bar&foo=baz" 11 | body = "{\"foo\": \"bar\"}" 12 | }) 13 | println(response.source.readString()) 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Gradle PR 3 | on: [pull_request] 4 | 5 | permissions: read-all 6 | jobs: 7 | gradle: 8 | runs-on: macos-latest 9 | steps: 10 | - uses: actions/checkout@v6 11 | - uses: actions/setup-java@v5 12 | with: 13 | distribution: 'temurin' 14 | java-version: '17' 15 | 16 | - name: Setup Gradle 17 | uses: gradle/actions/setup-gradle@v5 18 | 19 | - name: Gradle build 20 | run: ./gradlew build -x iosX64Test 21 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.js.generate.executable.default=false 3 | 4 | # Keep in sync with other projects 5 | # Give more memory to the Gradle daemon 6 | org.gradle.jvmargs=-Xmx4g 7 | 8 | artifactName=Fuel 9 | artifactDesc=The easiest HTTP networking library for Kotlin/Android 10 | artifactUrl=https://github.com/kittinunf/fuel 11 | artifactScm=git@github.com:kittinunf/fuel.git 12 | artifactLicenseName=MIT License 13 | artifactLicenseUrl=http://www.opensource.org/licenses/mit-license.php 14 | artifactPublishVersion=3.0.0-alpha04 15 | artifactGroupId=com.github.kittinunf.fuel 16 | -------------------------------------------------------------------------------- /samples/httpbin-wasm/src/wasmJsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import fuel.Fuel 2 | import fuel.get 3 | import kotlinx.browser.document 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.launch 7 | import kotlinx.dom.appendElement 8 | import kotlinx.dom.appendText 9 | import kotlinx.io.readString 10 | 11 | fun main() { 12 | document.body?.appendElement("div") { 13 | CoroutineScope(Dispatchers.Main).launch { 14 | val string = Fuel.get("http://httpbin.org/get").source.readString() 15 | appendText(string) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fuel/src/wasmJsMain/kotlin/fuel/Object.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | /** 4 | * Return empty JS Object 5 | */ 6 | public fun obj(): JsAny = js("{}") 7 | 8 | /** 9 | * Helper function for creating JavaScript objects with given type. 10 | */ 11 | public inline fun obj(init: T.() -> Unit): T = (obj().unsafeCast()).apply(init) 12 | 13 | /** 14 | * Operator to set property on JS Object 15 | */ 16 | public operator fun JsAny.set( 17 | key: String, 18 | value: JsAny, 19 | ) { 20 | this[key] = value 21 | } 22 | 23 | public fun getKeys(headers: org.w3c.fetch.Headers): JsArray = js("Array.from(headers.keys())") 24 | -------------------------------------------------------------------------------- /fuel-jackson-jvm/src/main/kotlin/fuel/jackson/ResponseExtension.kt: -------------------------------------------------------------------------------- 1 | package fuel.jackson 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper 5 | import com.fasterxml.jackson.module.kotlin.readValue 6 | import com.github.kittinunf.result.Result 7 | import com.github.kittinunf.result.runCatching 8 | import fuel.HttpResponse 9 | import kotlinx.io.readString 10 | 11 | public inline fun HttpResponse.toJackson(mapper: ObjectMapper = jacksonObjectMapper()): Result = 12 | runCatching { 13 | mapper.readValue(source.readString()) 14 | } 15 | -------------------------------------------------------------------------------- /fuel/src/jvmTest/kotlin/fuel/HttpLoaderFactoryTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import org.junit.Assert.assertFalse 4 | import java.util.concurrent.atomic.AtomicBoolean 5 | import kotlin.test.Test 6 | 7 | class HttpLoaderFactoryTest { 8 | @Test 9 | fun test_checkHttpLoaderInvokedOnce() { 10 | val httpLoader = FuelBuilder().build() 11 | 12 | val isInitialized = AtomicBoolean(false) 13 | Fuel.setHttpLoader { 14 | check(!isInitialized.getAndSet(true)) { "newHttpLoader was invoked more than once." } 15 | httpLoader 16 | } 17 | 18 | assertFalse(isInitialized.get()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /fuel/src/appleMain/kotlin/fuel/FuelBuilder.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import platform.Foundation.NSURLSessionConfiguration 4 | 5 | public actual class FuelBuilder { 6 | private var sessionConfiguration: NSURLSessionConfiguration? = null 7 | 8 | private val defaultSessionConfiguration by lazy { NSURLSessionConfiguration.defaultSessionConfiguration } 9 | 10 | public fun config(sessionConfiguration: NSURLSessionConfiguration): FuelBuilder = 11 | apply { 12 | this.sessionConfiguration = sessionConfiguration 13 | } 14 | 15 | public actual fun build(): HttpLoader = AppleHttpLoader(sessionConfiguration ?: defaultSessionConfiguration) 16 | } 17 | -------------------------------------------------------------------------------- /fuel/src/jvmMain/kotlin/fuel/FuelBuilder.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import okhttp3.Call 4 | import okhttp3.OkHttpClient 5 | 6 | public actual class FuelBuilder { 7 | private var callFactory: Lazy? = null 8 | 9 | public fun config(callFactory: Call.Factory): FuelBuilder = 10 | apply { 11 | this.callFactory = lazyOf(callFactory) 12 | } 13 | 14 | public fun config(initializer: () -> Call.Factory): FuelBuilder = 15 | apply { 16 | this.callFactory = lazy(initializer) 17 | } 18 | 19 | public actual fun build(): HttpLoader = JVMHttpLoader(callFactoryLazy = callFactory ?: lazy { OkHttpClient() }) 20 | } 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /fuel/src/jvmMain/kotlin/fuel/HttpUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | // Inspired By https://github.com/coil-kt/coil/blob/master/coil-base/src/main/java/coil/fetch/HttpFetcher.kt 2 | 3 | package fuel 4 | 5 | import okhttp3.Call 6 | import okhttp3.Request 7 | 8 | internal class HttpUrlFetcher( 9 | private val callFactory: Lazy, 10 | ) { 11 | fun fetch( 12 | request: fuel.Request, 13 | builder: Request.Builder, 14 | ): Call { 15 | if (request.parameters != null) { 16 | builder.url(request.url.fillURLWithParameters(request.parameters)) 17 | } else { 18 | builder.url(request.url) 19 | } 20 | return callFactory.value.newCall(builder.build()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /fuel-forge-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("publication") 6 | alias(libs.plugins.kover) 7 | } 8 | 9 | kotlin { 10 | explicitApi() 11 | compilerOptions { 12 | jvmTarget.set(JvmTarget.JVM_1_8) 13 | } 14 | } 15 | 16 | java { 17 | withSourcesJar() 18 | withJavadocJar() 19 | } 20 | 21 | tasks.withType { 22 | sourceCompatibility = JavaVersion.VERSION_1_8.toString() 23 | targetCompatibility = JavaVersion.VERSION_1_8.toString() 24 | } 25 | 26 | dependencies { 27 | api(project(":fuel")) 28 | api(libs.forge) 29 | api(libs.result.jvm) 30 | 31 | testImplementation(libs.junit) 32 | testImplementation(libs.mockwebserver) 33 | } 34 | -------------------------------------------------------------------------------- /fuel-jackson-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("publication") 6 | alias(libs.plugins.kover) 7 | } 8 | 9 | kotlin { 10 | explicitApi() 11 | compilerOptions { 12 | jvmTarget.set(JvmTarget.JVM_1_8) 13 | } 14 | } 15 | 16 | java { 17 | withSourcesJar() 18 | withJavadocJar() 19 | } 20 | 21 | tasks.withType { 22 | sourceCompatibility = JavaVersion.VERSION_1_8.toString() 23 | targetCompatibility = JavaVersion.VERSION_1_8.toString() 24 | } 25 | 26 | dependencies { 27 | api(project(":fuel")) 28 | api(libs.jackson.module.kotlin) 29 | api(libs.result.jvm) 30 | 31 | testImplementation(libs.junit) 32 | testImplementation(libs.mockwebserver) 33 | } 34 | -------------------------------------------------------------------------------- /fuel-kotlinx-serialization/src/commonMain/kotlin/fuel/serialization/ResponseExtensions.kt: -------------------------------------------------------------------------------- 1 | package fuel.serialization 2 | 3 | import com.github.kittinunf.result.Result 4 | import com.github.kittinunf.result.runCatching 5 | import fuel.HttpResponse 6 | import kotlinx.serialization.DeserializationStrategy 7 | import kotlinx.serialization.ExperimentalSerializationApi 8 | import kotlinx.serialization.json.Json 9 | import kotlinx.serialization.json.io.decodeFromSource 10 | 11 | @OptIn(ExperimentalSerializationApi::class) 12 | public fun HttpResponse.toJson( 13 | json: Json = Json, 14 | deserializationStrategy: DeserializationStrategy, 15 | ): Result = 16 | runCatching { 17 | json.decodeFromSource(source = source, deserializer = deserializationStrategy) 18 | } 19 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/HttpLoader.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | public interface HttpLoader { 6 | public suspend fun get(request: Request.Builder.() -> Unit): HttpResponse 7 | 8 | public suspend fun post(request: Request.Builder.() -> Unit): HttpResponse 9 | 10 | public suspend fun put(request: Request.Builder.() -> Unit): HttpResponse 11 | 12 | public suspend fun patch(request: Request.Builder.() -> Unit): HttpResponse 13 | 14 | public suspend fun delete(request: Request.Builder.() -> Unit): HttpResponse 15 | 16 | public suspend fun head(request: Request.Builder.() -> Unit): HttpResponse 17 | 18 | public suspend fun sse(request: Request.Builder.() -> Unit): Flow 19 | 20 | public suspend fun method(request: Request.Builder.() -> Unit): HttpResponse 21 | } 22 | -------------------------------------------------------------------------------- /fuel-moshi-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | kotlin("jvm") 5 | id("publication") 6 | alias(libs.plugins.kover) 7 | alias(libs.plugins.ksp) 8 | } 9 | 10 | kotlin { 11 | explicitApi() 12 | compilerOptions { 13 | jvmTarget.set(JvmTarget.JVM_1_8) 14 | } 15 | } 16 | 17 | java { 18 | withSourcesJar() 19 | withJavadocJar() 20 | } 21 | 22 | tasks.withType { 23 | sourceCompatibility = JavaVersion.VERSION_1_8.toString() 24 | targetCompatibility = JavaVersion.VERSION_1_8.toString() 25 | } 26 | 27 | dependencies { 28 | api(project(":fuel")) 29 | api(libs.moshi) 30 | api(libs.result.jvm) 31 | 32 | kspTest(libs.moshi.kotlin.codegen) 33 | 34 | testImplementation(libs.junit) 35 | testImplementation(libs.mockwebserver) 36 | } 37 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/Parameters.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public fun String.fillURLWithParameters(parameters: List>): String { 4 | val joiner = 5 | if (this.contains("?")) { 6 | if (parameters.isNotEmpty()) { 7 | "&" 8 | } else { 9 | // There is already a trailing ? 10 | "" 11 | } 12 | } else { 13 | "?" 14 | } 15 | return this + joiner + parameters.formUrlEncode() 16 | } 17 | 18 | private fun List>.formUrlEncode(): String = buildString { formUrlEncodeTo(this) } 19 | 20 | private fun List>.formUrlEncodeTo(out: Appendable) { 21 | joinTo(out, "&") { 22 | val key = UriCodec.encode(it.first) 23 | val value = UriCodec.encode(it.second) 24 | "$key=$value" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Gradle Push 3 | on: 4 | release: 5 | types: [released] 6 | 7 | permissions: read-all 8 | jobs: 9 | gradle: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/setup-java@v5 14 | with: 15 | distribution: 'temurin' 16 | java-version: '17' 17 | 18 | - name: Setup Gradle 19 | uses: gradle/actions/setup-gradle@v5 20 | 21 | - name: Gradle build 22 | run: ./gradlew build -x iosX64Test 23 | 24 | - name: Publish to MavenCentral 25 | env: 26 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 27 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 28 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 29 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 30 | run: ./gradlew publish --max-workers 1 -Prelease=true 31 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run Gradle Push 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | permissions: read-all 8 | jobs: 9 | gradle: 10 | runs-on: macos-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | - uses: actions/setup-java@v5 14 | with: 15 | distribution: 'temurin' 16 | java-version: '17' 17 | 18 | - name: Setup Gradle 19 | uses: gradle/actions/setup-gradle@v5 20 | 21 | - name: Gradle build 22 | run: ./gradlew build -x iosX64Test 23 | 24 | - name: Publish to SNAPSHOT 25 | env: 26 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 27 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 28 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 29 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 30 | run: ./gradlew publishAllPublicationsToSonatypeRepository --max-workers 1 31 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/Fuel.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public object Fuel { 4 | private var httpLoader: HttpLoader? = null 5 | private var httpLoaderFactory: HttpLoaderFactory? = null 6 | 7 | public fun loader(): HttpLoader = httpLoader ?: newHttpLoader() 8 | 9 | public fun setHttpLoader(loader: HttpLoader) { 10 | httpLoaderFactory = null 11 | httpLoader = loader 12 | } 13 | 14 | public fun setHttpLoader(factory: HttpLoaderFactory) { 15 | httpLoaderFactory = factory 16 | httpLoader = null 17 | } 18 | 19 | private fun newHttpLoader(): HttpLoader { 20 | // Check again in case httpLoader was just set. 21 | httpLoader?.let { return it } 22 | 23 | // Create a new HttpLoader. 24 | val loader = httpLoaderFactory?.newHttpLoader() ?: FuelBuilder().build() 25 | httpLoaderFactory = null 26 | httpLoader = loader 27 | return loader 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/Request.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public typealias Parameters = List> 4 | 5 | public class Request( 6 | public val url: String, 7 | public val parameters: Parameters?, 8 | public val headers: Map, 9 | public val body: String?, 10 | public val method: String?, 11 | ) { 12 | private constructor(builder: Builder) : this( 13 | checkNotNull(builder.url) { "url == null" }, 14 | builder.parameters, 15 | builder.headers, 16 | builder.body, 17 | builder.method, 18 | ) 19 | 20 | public class Builder { 21 | public var url: String? = null 22 | public var headers: Map = emptyMap() 23 | public var body: String? = null 24 | public var method: String? = null 25 | public var parameters: Parameters? = null 26 | 27 | /** 28 | * Create a new [Request] instance. 29 | */ 30 | public fun build(): Request = Request(this) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /fuel/src/commonTest/kotlin/fuel/ParametersTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class ParametersTest { 7 | @Test 8 | fun parametersWithQuestionMarkOnURL() { 9 | val test = "http://example.com?".fillURLWithParameters(listOf("test" to "url")) 10 | assertEquals("http://example.com?&test=url", test) 11 | } 12 | 13 | @Test 14 | fun parametersWithQuestionMarkOnParameter() { 15 | val test = "http://example.com".fillURLWithParameters(listOf("?test" to "url")) 16 | assertEquals("http://example.com?%3Ftest=url", test) 17 | } 18 | 19 | @Test 20 | fun parametersWithEmptyParameter() { 21 | val test = "http://example.com".fillURLWithParameters(listOf()) 22 | assertEquals("http://example.com?", test) 23 | } 24 | 25 | @Test 26 | fun parametersWithQuestionMarkAndEmptyParameter() { 27 | val test = "http://example.com?".fillURLWithParameters(listOf()) 28 | assertEquals("http://example.com?", test) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/httpbin-wasm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 2 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | } 7 | 8 | kotlin { 9 | @OptIn(ExperimentalWasmDsl::class) 10 | wasmJs { 11 | binaries.executable() 12 | browser { 13 | browser { 14 | commonWebpackConfig { 15 | devServer = 16 | (devServer ?: KotlinWebpackConfig.DevServer()).apply { 17 | static = 18 | (static ?: mutableListOf()).apply { 19 | add(project.rootDir.path) 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | sourceSets { 28 | commonMain { 29 | dependencies { 30 | implementation(project(":fuel")) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fuel-moshi-jvm/src/main/kotlin/fuel/moshi/ResponseExtension.kt: -------------------------------------------------------------------------------- 1 | package fuel.moshi 2 | 3 | import com.github.kittinunf.result.Result 4 | import com.github.kittinunf.result.runCatching 5 | import com.squareup.moshi.JsonAdapter 6 | import com.squareup.moshi.Moshi 7 | import fuel.HttpResponse 8 | import kotlinx.io.readByteArray 9 | import okio.Buffer 10 | import java.lang.reflect.Type 11 | 12 | public val defaultMoshi: Moshi.Builder = Moshi.Builder() 13 | 14 | public inline fun HttpResponse.toMoshi(): Result = toMoshi(T::class.java) 15 | 16 | public fun HttpResponse.toMoshi(clazz: Class): Result = toMoshi(defaultMoshi.build().adapter(clazz)) 17 | 18 | public fun HttpResponse.toMoshi(type: Type): Result = toMoshi(defaultMoshi.build().adapter(type)) 19 | 20 | public fun HttpResponse.toMoshi(jsonAdapter: JsonAdapter): Result { 21 | val buffer = Buffer().apply { write(source.readByteArray()) } 22 | return runCatching { 23 | jsonAdapter.fromJson(buffer) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /fuel/src/appleMain/kotlin/fuel/NSURLUtils.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.cinterop.BetaInteropApi 4 | import kotlinx.cinterop.ExperimentalForeignApi 5 | import kotlinx.cinterop.refTo 6 | import platform.Foundation.NSData 7 | import platform.Foundation.NSHTTPURLResponse 8 | import platform.Foundation.NSString 9 | import platform.Foundation.NSUTF8StringEncoding 10 | import platform.Foundation.create 11 | import platform.Foundation.dataUsingEncoding 12 | import platform.posix.memcpy 13 | 14 | public fun NSHTTPURLResponse.readHeaders(): Map { 15 | val map = mutableMapOf() 16 | allHeaderFields.forEach { 17 | map[it.key as String] = it.value as String 18 | } 19 | return map 20 | } 21 | 22 | @OptIn(ExperimentalForeignApi::class) 23 | public fun NSData.toByteArray(): ByteArray = 24 | ByteArray(length.toInt()).apply { 25 | if (isNotEmpty()) { 26 | memcpy(refTo(0), bytes, length) 27 | } 28 | } 29 | 30 | @BetaInteropApi 31 | public fun String.encode(): NSData = NSString.create(string = this).dataUsingEncoding(NSUTF8StringEncoding)!! 32 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/FuelRouting.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public interface FuelRouting : RequestConvertible { 4 | /** 5 | * Base path handler for the remote call. 6 | */ 7 | public val basePath: String 8 | 9 | /** 10 | * Method handler for the remote requests. 11 | */ 12 | public val method: String 13 | 14 | /** 15 | * Path handler for the request. 16 | */ 17 | public val path: String 18 | 19 | /** 20 | * Parameters for the remote call. 21 | * It uses a pair with String, String. 22 | */ 23 | public val parameters: Parameters? 24 | 25 | /** 26 | * Headers for remote call. 27 | */ 28 | public val headers: Map? 29 | 30 | /** 31 | * Body to handle other type of request (e.g. application/json ) 32 | */ 33 | public val body: String? 34 | 35 | override val request: Request 36 | get() = 37 | Request( 38 | "$basePath/$path", 39 | parameters, 40 | headers.orEmpty(), 41 | body, 42 | method, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kittinun Vantasin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /fuel/src/commonTest/kotlin/fuel/RequestTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class RequestTest { 7 | @Test 8 | fun testAdd_headers() { 9 | val request = 10 | Request 11 | .Builder() 12 | .apply { 13 | url = "http://example.com" 14 | headers = mapOf("X-Test" to "true") 15 | }.build() 16 | assertEquals("true", request.headers["X-Test"]) 17 | } 18 | 19 | @Test 20 | fun test_errorsWhenDataIsEmpty() { 21 | try { 22 | Request.Builder().build() 23 | } catch (ise: IllegalStateException) { 24 | assertEquals("url == null", ise.message) 25 | } 26 | } 27 | 28 | /*@Test 29 | fun `invalid web socket as non secure url`() { 30 | val request = Request.Builder() 31 | .url("ws://google.com") 32 | .build() 33 | assertEquals("http://google.com", request.url) 34 | } 35 | 36 | @Test 37 | fun `invalid web socket as secure url`() { 38 | val request = Request.Builder() 39 | .url("wss://google.com") 40 | .build() 41 | assertEquals("https://google.com", request.url) 42 | }*/ 43 | } 44 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/Strings.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | public suspend fun String.httpGet( 6 | parameters: Parameters? = null, 7 | headers: Map = emptyMap(), 8 | ): HttpResponse = Fuel.get(this, parameters, headers) 9 | 10 | public suspend fun String.httpPost( 11 | parameters: Parameters? = null, 12 | body: String? = null, 13 | headers: Map = emptyMap(), 14 | ): HttpResponse = Fuel.post(this, parameters, body, headers) 15 | 16 | public suspend fun String.httpPut( 17 | parameters: Parameters? = null, 18 | body: String? = null, 19 | headers: Map = emptyMap(), 20 | ): HttpResponse = Fuel.put(this, parameters, body, headers) 21 | 22 | public suspend fun String.httpPatch( 23 | parameters: Parameters? = null, 24 | body: String? = null, 25 | headers: Map = emptyMap(), 26 | ): HttpResponse = Fuel.patch(this, parameters, body, headers) 27 | 28 | public suspend fun String.httpDelete( 29 | parameters: Parameters? = null, 30 | body: String? = null, 31 | headers: Map = emptyMap(), 32 | ): HttpResponse = Fuel.delete(this, parameters, body, headers) 33 | 34 | public suspend fun String.httpHead(parameters: Parameters? = null): HttpResponse = Fuel.head(this, parameters) 35 | 36 | public suspend fun String.httpSSE( 37 | parameters: Parameters?, 38 | headers: Map = emptyMap(), 39 | ): Flow = Fuel.sse(this, parameters, headers) 40 | 41 | public suspend fun String.httpMethod( 42 | parameters: Parameters? = null, 43 | method: String, 44 | body: String? = null, 45 | headers: Map = emptyMap(), 46 | ): HttpResponse = Fuel.method(this, parameters, method, body, headers) 47 | -------------------------------------------------------------------------------- /fuel-forge-jvm/src/test/kotlin/fuel/forge/FuelForgeTest.kt: -------------------------------------------------------------------------------- 1 | package fuel.forge 2 | 3 | import com.github.kittinunf.forge.core.JSON 4 | import com.github.kittinunf.forge.core.apply 5 | import com.github.kittinunf.forge.core.at 6 | import com.github.kittinunf.forge.core.map 7 | import com.github.kittinunf.forge.util.create 8 | import com.github.kittinunf.result.Result 9 | import fuel.Fuel 10 | import fuel.get 11 | import kotlinx.coroutines.runBlocking 12 | import mockwebserver3.MockResponse 13 | import mockwebserver3.MockWebServer 14 | import okhttp3.ExperimentalOkHttpApi 15 | import org.junit.Assert.assertEquals 16 | import org.junit.Assert.fail 17 | import org.junit.Test 18 | 19 | class FuelForgeTest { 20 | data class HttpBinUserAgentModel( 21 | var userAgent: String = "", 22 | var status: String = "", 23 | ) 24 | 25 | private val httpBinUserDeserializer = { json: JSON -> 26 | ::HttpBinUserAgentModel.create 27 | .map(json at "userAgent") 28 | .apply(json at "status") 29 | } 30 | 31 | @OptIn(ExperimentalOkHttpApi::class) 32 | @Test 33 | fun testForgeResponse() = 34 | runBlocking { 35 | val mockWebServer = 36 | MockWebServer().apply { 37 | enqueue(MockResponse(body = "{\"userAgent\": \"Fuel\", \"status\": \"OK\"}")) 38 | start() 39 | } 40 | 41 | val binUserAgentModel = HttpBinUserAgentModel("Fuel", "OK") 42 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 43 | when (val forge = response.toForge(httpBinUserDeserializer)) { 44 | is Result.Success -> assertEquals(binUserAgentModel, forge.value) 45 | is Result.Failure -> fail(forge.error.localizedMessage) 46 | } 47 | 48 | mockWebServer.shutdown() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ################################# 3 | ################################# 4 | ## Super Linter GitHub Actions ## 5 | ################################# 6 | ################################# 7 | name: Lint Code Base 8 | 9 | ############################# 10 | # Start the job on all push # 11 | ############################# 12 | on: 13 | push: 14 | branches-ignore: [master, main] 15 | # Remove the line above to run when pushing to master or main 16 | # pull_request: 17 | # branches: [master, main] 18 | 19 | ############### 20 | # Set the Job # 21 | ############### 22 | permissions: read-all 23 | jobs: 24 | build: 25 | # Name the Job 26 | name: Lint Code Base 27 | # Set the agent to run on 28 | runs-on: ubuntu-latest 29 | 30 | ############################################ 31 | # Grant status permission for MULTI_STATUS # 32 | ############################################ 33 | permissions: 34 | contents: read 35 | packages: read 36 | statuses: write 37 | 38 | ################## 39 | # Load all steps # 40 | ################## 41 | steps: 42 | ########################## 43 | # Checkout the code base # 44 | ########################## 45 | - name: Checkout Code 46 | uses: actions/checkout@v6 47 | with: 48 | # Full git history is needed to get a proper 49 | # list of changed files within `super-linter` 50 | fetch-depth: 0 51 | 52 | ################################ 53 | # Run Linter against code base # 54 | ################################ 55 | - name: Lint Code Base 56 | uses: super-linter/super-linter/slim@v8 57 | env: 58 | VALIDATE_ALL_CODEBASE: false 59 | VALIDATE_JSCPD: false 60 | DEFAULT_BRANCH: main 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | -------------------------------------------------------------------------------- /fuel-kotlinx-serialization/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | alias(libs.plugins.serialization) 6 | id("publication") 7 | alias(libs.plugins.kover) 8 | } 9 | 10 | kotlin { 11 | jvm { 12 | compilerOptions { 13 | jvmTarget.set(JvmTarget.JVM_1_8) 14 | } 15 | testRuns["test"].executionTask.configure { 16 | useJUnit() 17 | } 18 | } 19 | iosArm64 { 20 | binaries { 21 | framework { 22 | baseName = "Fuel-Serialization" 23 | } 24 | } 25 | } 26 | macosArm64 { 27 | binaries { 28 | framework { 29 | baseName = "Fuel-Serialization" 30 | } 31 | } 32 | } 33 | iosX64 { 34 | binaries { 35 | framework { 36 | baseName = "Fuel-Serialization" 37 | } 38 | } 39 | } 40 | macosX64 { 41 | binaries { 42 | framework { 43 | baseName = "Fuel-Serialization" 44 | } 45 | } 46 | } 47 | iosSimulatorArm64 { 48 | binaries { 49 | framework { 50 | baseName = "Fuel-Serialization" 51 | } 52 | } 53 | } 54 | 55 | explicitApi() 56 | 57 | sourceSets { 58 | commonMain { 59 | dependencies { 60 | api(project(":fuel")) 61 | api(libs.kotlinx.serialization.json) 62 | api(libs.result) 63 | } 64 | } 65 | commonTest { 66 | dependencies { 67 | implementation(kotlin("test")) 68 | } 69 | } 70 | jvmTest { 71 | dependencies { 72 | implementation(libs.mockwebserver) 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/UriSyntaxException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * Copyright (C) 2022 Eliezer Graber 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package fuel 19 | 20 | /** 21 | * Exception thrown to indicate that a string could not be parsed as a URI reference. 22 | */ 23 | public class UriSyntaxException( 24 | /** 25 | * The input string. 26 | */ 27 | public val input: String, 28 | /** 29 | * Returns a string explaining why the input string could not be parsed. 30 | */ 31 | private val internalReason: String, 32 | /** 33 | * An index into the input string of the position at which the 34 | * parse error occurred, or `-1` if this position is not known. 35 | */ 36 | public val index: Int = -1, 37 | ) : Exception(internalReason) { 38 | init { 39 | require(index >= -1) 40 | } 41 | 42 | public val reason: String 43 | get() = message 44 | 45 | public override val message: String 46 | get() = 47 | buildString { 48 | append(internalReason) 49 | if (index > -1) { 50 | append(" at index ") 51 | append(index) 52 | } 53 | append(": ") 54 | append(input) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | coroutines = "1.10.2" 3 | jackson = "2.20.1" 4 | junit = "4.13.2" 5 | moshi = "1.15.2" 6 | okhttp = "5.0.0-alpha.16" 7 | forge = "1.0.0-alpha3" 8 | result = "5.6.0" 9 | serialization = "1.9.0" 10 | kotlinx-io = "0.8.2" 11 | kover = "0.9.4" 12 | kotlin = "2.2.21" 13 | ksp = "2.3.3" 14 | 15 | [libraries] 16 | kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } 17 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json-io", version.ref = "serialization" } 18 | kotlinx-io-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-io-core", version.ref = "kotlinx-io" } 19 | jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson" } 20 | junit = { group = "junit", name = "junit", version.ref = "junit" } 21 | mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver", version.ref = "okhttp" } 22 | moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" } 23 | moshi-kotlin-codegen = { group = "com.squareup.moshi", name = "moshi-kotlin-codegen", version.ref = "moshi" } 24 | okhttp-coroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } 25 | forge = { group = "com.github.kittinunf.forge", name = "forge", version.ref = "forge" } 26 | result = { group = "com.github.kittinunf.result", name = "result", version.ref = "result"} 27 | result-jvm = { group = "com.github.kittinunf.result", name = "result-jvm", version.ref = "result"} 28 | 29 | [plugins] 30 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 31 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 32 | serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 33 | ksp = {id = "com.google.devtools.ksp", version.ref="ksp" } 34 | -------------------------------------------------------------------------------- /fuel-kotlinx-serialization/src/jvmTest/kotlin/fuel/serialization/FuelKotlinxSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package fuel.serialization 2 | 3 | import fuel.Fuel 4 | import fuel.get 5 | import kotlinx.coroutines.runBlocking 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.Json 8 | import mockwebserver3.MockResponse 9 | import mockwebserver3.MockWebServer 10 | import okhttp3.ExperimentalOkHttpApi 11 | import org.junit.Assert.assertEquals 12 | import org.junit.Test 13 | import kotlin.test.fail 14 | 15 | @OptIn(ExperimentalOkHttpApi::class) 16 | class FuelKotlinxSerializationTest { 17 | @Serializable 18 | data class HttpBinUserAgentModel( 19 | var userAgent: String = "", 20 | ) 21 | 22 | @Test 23 | fun testSerializableResponse() = 24 | runBlocking { 25 | val mockWebServer = 26 | MockWebServer().apply { 27 | enqueue(MockResponse(body = "{\"userAgent\": \"Fuel\"}")) 28 | start() 29 | } 30 | 31 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 32 | val json = response.toJson(Json.Default, HttpBinUserAgentModel.serializer()) 33 | json.fold({ 34 | assertEquals("Fuel", it?.userAgent) 35 | }, { 36 | fail(it.message) 37 | }) 38 | 39 | mockWebServer.shutdown() 40 | } 41 | 42 | @Test 43 | fun testSerializableResponseWithDefaultJson() = 44 | runBlocking { 45 | val mockWebServer = 46 | MockWebServer().apply { 47 | enqueue(MockResponse(body = "{\"userAgent\": \"Fuel2\"}")) 48 | start() 49 | } 50 | 51 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 52 | val json = response.toJson(deserializationStrategy = HttpBinUserAgentModel.serializer()) 53 | json.fold({ 54 | assertEquals("Fuel2", it?.userAgent) 55 | }, { 56 | fail(it.message) 57 | }) 58 | 59 | mockWebServer.shutdown() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /fuel/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.multiplatform) 5 | id("publication") 6 | alias(libs.plugins.kover) 7 | } 8 | 9 | kotlin { 10 | jvm { 11 | compilerOptions { 12 | jvmTarget.set(JvmTarget.JVM_1_8) 13 | } 14 | testRuns["test"].executionTask.configure { 15 | useJUnit() 16 | } 17 | } 18 | iosArm64 { 19 | binaries { 20 | framework { 21 | baseName = "Fuel" 22 | } 23 | } 24 | } 25 | macosArm64 { 26 | binaries { 27 | framework { 28 | baseName = "Fuel" 29 | } 30 | } 31 | } 32 | iosX64 { 33 | binaries { 34 | framework { 35 | baseName = "Fuel" 36 | } 37 | } 38 | } 39 | macosX64 { 40 | binaries { 41 | framework { 42 | baseName = "Fuel" 43 | } 44 | } 45 | } 46 | iosSimulatorArm64 { 47 | binaries { 48 | framework { 49 | baseName = "Fuel" 50 | } 51 | } 52 | } 53 | 54 | explicitApi() 55 | 56 | compilerOptions { 57 | freeCompilerArgs.add("-Xexpect-actual-classes") 58 | } 59 | 60 | sourceSets { 61 | commonMain { 62 | dependencies { 63 | api(libs.kotlinx.coroutines.core) 64 | api(libs.kotlinx.io.core) 65 | } 66 | } 67 | commonTest { 68 | dependencies { 69 | implementation(kotlin("test")) 70 | } 71 | } 72 | 73 | jvmMain { 74 | dependencies { 75 | api(libs.okhttp.coroutines) 76 | } 77 | } 78 | jvmTest { 79 | dependencies { 80 | implementation(libs.mockwebserver) 81 | } 82 | } 83 | } 84 | } 85 | 86 | dependencies { 87 | kover(project(":fuel-forge-jvm")) 88 | kover(project(":fuel-jackson-jvm")) 89 | kover(project(":fuel-kotlinx-serialization")) 90 | kover(project(":fuel-moshi-jvm")) 91 | } 92 | -------------------------------------------------------------------------------- /fuel/src/jvmMain/kotlin/fuel/OkHttpUtils.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.channels.awaitClose 6 | import kotlinx.coroutines.channels.trySendBlocking 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.callbackFlow 9 | import kotlinx.coroutines.withContext 10 | import kotlinx.io.Buffer 11 | import okhttp3.Call 12 | import okhttp3.Response 13 | import okhttp3.coroutines.executeAsync 14 | 15 | @OptIn(ExperimentalCoroutinesApi::class) 16 | public suspend fun Call.performAsync(): HttpResponse = 17 | withContext(Dispatchers.IO) { 18 | executeAsync().use { response -> 19 | val sourceBuffer = Buffer() 20 | sourceBuffer.write(response.body.bytes()) 21 | HttpResponse().apply { 22 | statusCode = response.code 23 | source = sourceBuffer 24 | headers = response.toHeaders() 25 | } 26 | } 27 | } 28 | 29 | @OptIn(ExperimentalCoroutinesApi::class) 30 | public fun Call.performAsyncWithSSE(): Flow = 31 | callbackFlow { 32 | val response = executeAsync() 33 | val reader = response.body.byteStream().bufferedReader() 34 | 35 | try { 36 | reader.useLines { lines -> 37 | for (line in lines) { 38 | if (line.startsWith("data:")) { 39 | val event = line.removePrefix("data:").trim() 40 | trySendBlocking(event) // Ensure event is sent 41 | } 42 | } 43 | } 44 | } catch (e: Exception) { 45 | close(e) // Properly close on error 46 | } finally { 47 | response.close() // Explicitly close HTTP response 48 | } 49 | 50 | awaitClose { 51 | response.close() // Ensure proper cleanup when the flow is cancelled 52 | } 53 | } 54 | 55 | public fun Response.toHeaders(): Map = 56 | headers 57 | .names() 58 | .mapNotNull { name -> 59 | headers[name]?.let { value -> name to value } 60 | }.toMap() 61 | -------------------------------------------------------------------------------- /fuel/src/wasmJsMain/kotlin/fuel/WasmHttpLoader.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | public class WasmHttpLoader : HttpLoader { 4 | private val fetcher by lazy { HttpUrlFetcher() } 5 | 6 | public override suspend fun get(request: Request.Builder.() -> Unit): HttpResponse { 7 | val requestBuilder = Request.Builder().apply(request).build() 8 | return fetcher.fetch(requestBuilder, "GET") 9 | } 10 | 11 | public override suspend fun post(request: Request.Builder.() -> Unit): HttpResponse { 12 | val requestBuilder = Request.Builder().apply(request).build() 13 | requireNotNull(requestBuilder.body) { "body for method POST should not be null" } 14 | return fetcher.fetch(requestBuilder, "POST", requestBuilder.body) 15 | } 16 | 17 | public override suspend fun put(request: Request.Builder.() -> Unit): HttpResponse { 18 | val requestBuilder = Request.Builder().apply(request).build() 19 | requireNotNull(requestBuilder.body) { "body for method POST should not be null" } 20 | return fetcher.fetch(requestBuilder, "PUT", requestBuilder.body) 21 | } 22 | 23 | public override suspend fun patch(request: Request.Builder.() -> Unit): HttpResponse { 24 | val requestBuilder = Request.Builder().apply(request).build() 25 | requireNotNull(requestBuilder.body) { "body for method POST should not be null" } 26 | return fetcher.fetch(requestBuilder, "PATCH", requestBuilder.body) 27 | } 28 | 29 | public override suspend fun delete(request: Request.Builder.() -> Unit): HttpResponse { 30 | val requestBuilder = Request.Builder().apply(request).build() 31 | return fetcher.fetch(requestBuilder, "DELETE") 32 | } 33 | 34 | public override suspend fun head(request: Request.Builder.() -> Unit): HttpResponse { 35 | val requestBuilder = Request.Builder().apply(request).build() 36 | return fetcher.fetch(requestBuilder, "HEAD") 37 | } 38 | 39 | public override suspend fun method(request: Request.Builder.() -> Unit): HttpResponse { 40 | val requestBuilder = Request.Builder().apply(request).build() 41 | val method = requireNotNull(requestBuilder.method) { "method should be not null" } 42 | return fetcher.fetch(requestBuilder, method, requestBuilder.body) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /fuel-kotlinx-serialization/src/appleTest/kotlin/fuel/serialization/FuelKotlinxSerializationTest.kt: -------------------------------------------------------------------------------- 1 | package fuel.serialization 2 | 3 | import fuel.HttpResponse 4 | import kotlinx.io.Buffer 5 | import kotlinx.io.writeString 6 | import kotlinx.serialization.Serializable 7 | import kotlinx.serialization.json.Json 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | import kotlin.test.fail 11 | 12 | class FuelKotlinxSerializationTest { 13 | @Serializable 14 | data class HttpBinUserAgentModel( 15 | var userAgent: String = "", 16 | ) 17 | 18 | @Serializable 19 | data class RocketLaunch( 20 | var rocket: String, 21 | var success: Boolean, 22 | var details: String, 23 | ) 24 | 25 | @Test 26 | fun testSerializableResponse() { 27 | val jsonBuffer = Buffer().also { it.writeString("{\"userAgent\": \"Fuel\"}") } 28 | val httpResponse = 29 | HttpResponse().apply { 30 | statusCode = 200 31 | source = jsonBuffer 32 | } 33 | val json = httpResponse.toJson(Json.Default, HttpBinUserAgentModel.serializer()) 34 | json.fold({ 35 | assertEquals("Fuel", it?.userAgent) 36 | }, { 37 | fail(it.message) 38 | }) 39 | } 40 | 41 | @Test 42 | fun testSpaceXDetail() { 43 | val jsonBuffer = 44 | Buffer().also { 45 | it.writeString( 46 | "{\"rocket\":\"5e9d0d95eda69973a809d1ec\", \"success\":true,\"details\":\"Second GTO launch for Falcon 9. The USAF evaluated launch data from this flight as part of a separate certification program for SpaceX to qualify to fly U.S. military payloads and found that the Thaicom 6 launch had \\\"unacceptable fuel reserves at engine cutoff of the stage 2 second burnoff\\\"\"}", 47 | ) 48 | } 49 | val httpResponse = 50 | HttpResponse().apply { 51 | statusCode = 200 52 | source = jsonBuffer 53 | } 54 | val json = httpResponse.toJson(Json.Default, RocketLaunch.serializer()) 55 | json.fold({ 56 | assertEquals( 57 | "Second GTO launch for Falcon 9. The USAF evaluated launch data from this flight as part of a separate certification program for SpaceX to qualify to fly U.S. military payloads and found that the Thaicom 6 launch had \"unacceptable fuel reserves at engine cutoff of the stage 2 second burnoff\"", 58 | it?.details, 59 | ) 60 | }, { 61 | fail(it.message) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /fuel-jackson-jvm/src/test/kotlin/fuel/jackson/FuelJacksonTest.kt: -------------------------------------------------------------------------------- 1 | package fuel.jackson 2 | 3 | import com.fasterxml.jackson.databind.DeserializationFeature 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategies 6 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule 7 | import fuel.Fuel 8 | import fuel.get 9 | import kotlinx.coroutines.runBlocking 10 | import mockwebserver3.MockResponse 11 | import mockwebserver3.MockWebServer 12 | import okhttp3.ExperimentalOkHttpApi 13 | import org.junit.Assert.assertEquals 14 | import org.junit.Assert.fail 15 | import org.junit.Test 16 | 17 | @ExperimentalOkHttpApi 18 | class FuelJacksonTest { 19 | private val createCustomMapper: ObjectMapper = 20 | ObjectMapper() 21 | .registerKotlinModule() 22 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 23 | .apply { 24 | propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE 25 | } 26 | 27 | data class HttpBinUserAgentModel( 28 | val userAgent: String = "", 29 | val http_status: String = "", 30 | ) 31 | 32 | @Test 33 | fun jacksonTestResponseObject() = 34 | runBlocking { 35 | val mockWebServer = 36 | MockWebServer().apply { 37 | enqueue(MockResponse(body = "{\"userAgent\": \"Fuel\"}")) 38 | start() 39 | } 40 | 41 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 42 | val jackson = response.toJackson() 43 | jackson.fold({ 44 | assertEquals("Fuel", it?.userAgent) 45 | }, { 46 | fail(it.localizedMessage) 47 | }) 48 | 49 | mockWebServer.shutdown() 50 | } 51 | 52 | @Test 53 | fun jacksonTestResponseObjectWithCustomMapper() = 54 | runBlocking { 55 | val mockWebServer = 56 | MockWebServer().apply { 57 | enqueue(MockResponse(body = "{\"userAgent\": \"Fuel\", \"http_status\": \"OK\"}")) 58 | start() 59 | } 60 | 61 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 62 | val jackson = response.toJackson(createCustomMapper) 63 | jackson.fold({ 64 | assertEquals("", it?.userAgent) 65 | assertEquals("OK", it?.http_status) 66 | }, { 67 | fail(it.localizedMessage) 68 | }) 69 | 70 | mockWebServer.shutdown() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /fuel/src/appleMain/kotlin/fuel/AppleHttpLoader.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import platform.Foundation.NSURLSessionConfiguration 5 | 6 | public class AppleHttpLoader( 7 | sessionConfiguration: NSURLSessionConfiguration, 8 | ) : HttpLoader { 9 | private val fetcher by lazy { HttpUrlFetcher(sessionConfiguration) } 10 | 11 | public override suspend fun get(request: Request.Builder.() -> Unit): HttpResponse { 12 | val requestBuilder = Request.Builder().apply(request).build() 13 | return fetcher.fetch("GET", requestBuilder) 14 | } 15 | 16 | public override suspend fun post(request: Request.Builder.() -> Unit): HttpResponse { 17 | val requestBuilder = Request.Builder().apply(request).build() 18 | requireNotNull(requestBuilder.body) { "body for method POST should not be null" } 19 | return fetcher.fetch("POST", requestBuilder) 20 | } 21 | 22 | public override suspend fun put(request: Request.Builder.() -> Unit): HttpResponse { 23 | val requestBuilder = Request.Builder().apply(request).build() 24 | requireNotNull(requestBuilder.body) { "body for method PUT should not be null" } 25 | return fetcher.fetch("PUT", requestBuilder) 26 | } 27 | 28 | public override suspend fun patch(request: Request.Builder.() -> Unit): HttpResponse { 29 | val requestBuilder = Request.Builder().apply(request).build() 30 | requireNotNull(requestBuilder.body) { "body for method PATCH should not be null" } 31 | return fetcher.fetch("PATCH", requestBuilder) 32 | } 33 | 34 | public override suspend fun delete(request: Request.Builder.() -> Unit): HttpResponse { 35 | val requestBuilder = Request.Builder().apply(request).build() 36 | return fetcher.fetch("DELETE", requestBuilder) 37 | } 38 | 39 | public override suspend fun head(request: Request.Builder.() -> Unit): HttpResponse { 40 | val requestBuilder = Request.Builder().apply(request).build() 41 | return fetcher.fetch("HEAD", requestBuilder) 42 | } 43 | 44 | override suspend fun sse(request: Request.Builder.() -> Unit): Flow { 45 | val requestBuilder = Request.Builder().apply(request).build() 46 | return fetcher.fetchSSE(requestBuilder) 47 | } 48 | 49 | public override suspend fun method(request: Request.Builder.() -> Unit): HttpResponse { 50 | val requestBuilder = Request.Builder().apply(request).build() 51 | val method = requireNotNull(requestBuilder.method) { "method should be not null" } 52 | return fetcher.fetch(method, requestBuilder) 53 | } 54 | 55 | public companion object { 56 | public operator fun invoke(): HttpLoader = FuelBuilder().build() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /fuel/src/jvmTest/kotlin/fuel/RoutingTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.io.readString 5 | import mockwebserver3.MockResponse 6 | import mockwebserver3.MockWebServer 7 | import okhttp3.ExperimentalOkHttpApi 8 | import org.junit.After 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Before 11 | import org.junit.Test 12 | 13 | @OptIn(ExperimentalOkHttpApi::class) 14 | class RoutingTest { 15 | sealed class TestApi( 16 | private val host: String, 17 | ) : FuelRouting { 18 | override val basePath = this.host 19 | 20 | class GetTest( 21 | host: String, 22 | ) : TestApi(host) 23 | 24 | class GetParamsTest( 25 | host: String, 26 | ) : TestApi(host) 27 | 28 | override val parameters: Parameters? 29 | get() = null 30 | 31 | override val method: String 32 | get() { 33 | return when (this) { 34 | is GetTest -> "GET" 35 | is GetParamsTest -> "GET" 36 | } 37 | } 38 | 39 | override val path: String 40 | get() { 41 | return when (this) { 42 | is GetTest -> "/get" 43 | is GetParamsTest -> "/get?foo=bar" 44 | } 45 | } 46 | 47 | override val body: String? 48 | get() = null 49 | 50 | override val headers: Map? 51 | get() { 52 | return when (this) { 53 | is GetTest -> null 54 | is GetParamsTest -> mapOf("X-Test" to "true") 55 | } 56 | } 57 | } 58 | 59 | private lateinit var mockWebServer: MockWebServer 60 | 61 | @Before 62 | fun setUp() { 63 | mockWebServer = MockWebServer().apply { start() } 64 | } 65 | 66 | @After 67 | fun tearDown() { 68 | mockWebServer.shutdown() 69 | } 70 | 71 | @Test 72 | fun httpRouterGet() = 73 | runBlocking { 74 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 75 | 76 | val getTest = TestApi.GetTest(mockWebServer.url("").toString()) 77 | val response = Fuel.request(getTest).source.readString() 78 | val request1 = mockWebServer.takeRequest() 79 | 80 | assertEquals("Hello World", response) 81 | assertEquals("GET", request1.method) 82 | } 83 | 84 | @Test 85 | fun httpRouterGetParams() = 86 | runBlocking { 87 | mockWebServer.enqueue(MockResponse(body = "Hello World With Params")) 88 | 89 | val getTest = TestApi.GetParamsTest(mockWebServer.url("").toString()) 90 | val response = Fuel.request(getTest).source.readString() 91 | val request1 = mockWebServer.takeRequest() 92 | 93 | assertEquals("Hello World With Params", response) 94 | assertEquals("GET", request1.method) 95 | assertEquals("///get?foo=bar", request1.path) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /fuel/src/wasmJsMain/kotlin/fuel/HttpUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.browser.window 4 | import kotlinx.coroutines.suspendCancellableCoroutine 5 | import kotlinx.io.Buffer 6 | import org.khronos.webgl.ArrayBuffer 7 | import org.khronos.webgl.Uint8Array 8 | import org.khronos.webgl.get 9 | import org.w3c.fetch.Headers 10 | import org.w3c.fetch.RequestInit 11 | import kotlin.coroutines.resume 12 | import kotlin.coroutines.resumeWithException 13 | 14 | internal class HttpUrlFetcher { 15 | suspend fun fetch( 16 | request: Request, 17 | method: String?, 18 | body: String? = null, 19 | ): HttpResponse { 20 | val urlString = 21 | request.parameters?.let { 22 | request.url.fillURLWithParameters(it) 23 | } ?: request.url 24 | 25 | val requestInit = obj {} 26 | requestInit.method = method 27 | requestInit.headers = request.headers.toJsReference() 28 | requestInit.body = body?.toJsString() 29 | return suspendCancellableCoroutine { continuation -> 30 | window 31 | .fetch(urlString, requestInit) 32 | .then { response -> 33 | if (response.ok) { 34 | response 35 | .arrayBuffer() 36 | .then { arrayBuffer -> 37 | val byteArray = arrayBuffer.toBuffer() 38 | continuation.resume( 39 | HttpResponse().apply { 40 | statusCode = response.status.toInt() 41 | source = byteArray 42 | headers = response.headers.mapToFuel() 43 | }, 44 | ) 45 | null 46 | } 47 | null 48 | } else { 49 | continuation.resumeWithException(Exception("Failed to fetch data: ${response.status}")) 50 | null 51 | } 52 | }.catch { 53 | continuation.resumeWithException(Exception("Failed to fetch data: $it")) 54 | null 55 | } 56 | } 57 | } 58 | 59 | private fun ArrayBuffer.toBuffer(): Buffer { 60 | val uint8Array = Uint8Array(this) 61 | val buffer = Buffer() 62 | for (i in 0 until uint8Array.length) { 63 | buffer.writeByte(uint8Array[i].toInt().toByte()) 64 | } 65 | return buffer 66 | } 67 | 68 | private fun Headers.mapToFuel(): Map { 69 | val headers = mutableMapOf() 70 | val keys = getKeys(this@mapToFuel) 71 | for (i in 0 until keys.length) { 72 | val key = keys[i].toString() 73 | val value = this@mapToFuel.get(key)!! 74 | headers[key] = value 75 | } 76 | return headers 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/Fuels.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | public suspend fun Fuel.get( 6 | url: String, 7 | parameters: Parameters? = null, 8 | headers: Map = emptyMap(), 9 | ): HttpResponse = 10 | loader().get { 11 | this.url = url 12 | this.parameters = parameters 13 | this.headers = headers 14 | } 15 | 16 | public suspend fun Fuel.post( 17 | url: String, 18 | parameters: Parameters? = null, 19 | body: String? = null, 20 | headers: Map = emptyMap(), 21 | ): HttpResponse = 22 | loader().post { 23 | this.url = url 24 | this.parameters = parameters 25 | this.body = body 26 | this.headers = headers 27 | } 28 | 29 | public suspend fun Fuel.put( 30 | url: String, 31 | parameters: Parameters? = null, 32 | body: String? = null, 33 | headers: Map = emptyMap(), 34 | ): HttpResponse = 35 | loader().put { 36 | this.url = url 37 | this.parameters = parameters 38 | this.headers = headers 39 | this.body = body 40 | } 41 | 42 | public suspend fun Fuel.patch( 43 | url: String, 44 | parameters: Parameters? = null, 45 | body: String? = null, 46 | headers: Map = emptyMap(), 47 | ): HttpResponse = 48 | loader().patch { 49 | this.url = url 50 | this.parameters = parameters 51 | this.body = body 52 | this.headers = headers 53 | } 54 | 55 | public suspend fun Fuel.delete( 56 | url: String, 57 | parameters: Parameters? = null, 58 | body: String? = null, 59 | headers: Map = emptyMap(), 60 | ): HttpResponse = 61 | loader().delete { 62 | this.url = url 63 | this.parameters = parameters 64 | this.body = body 65 | this.headers = headers 66 | } 67 | 68 | public suspend fun Fuel.head( 69 | url: String, 70 | parameters: Parameters? = null, 71 | ): HttpResponse = 72 | loader().head { 73 | this.url = url 74 | this.parameters = parameters 75 | } 76 | 77 | public suspend fun Fuel.method( 78 | url: String, 79 | parameters: Parameters? = null, 80 | method: String? = null, 81 | body: String? = null, 82 | headers: Map = emptyMap(), 83 | ): HttpResponse = 84 | loader().method { 85 | this.url = url 86 | this.parameters = parameters 87 | this.method = method 88 | this.body = body 89 | this.headers = headers 90 | } 91 | 92 | public suspend fun Fuel.sse( 93 | url: String, 94 | parameters: Parameters?, 95 | headers: Map, 96 | ): Flow = 97 | loader().sse { 98 | this.url = url 99 | this.parameters = parameters 100 | this.headers = headers 101 | } 102 | 103 | public suspend fun Fuel.request(convertible: RequestConvertible): HttpResponse { 104 | val request = convertible.request 105 | return loader().method { 106 | url = request.url 107 | parameters = request.parameters 108 | method = request.method 109 | body = request.body 110 | headers = request.headers 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /fuel/src/jvmMain/kotlin/fuel/JVMHttpLoader.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import okhttp3.Call 5 | import okhttp3.Request.Builder 6 | import okhttp3.RequestBody.Companion.toRequestBody 7 | import okhttp3.internal.http.HttpMethod 8 | 9 | public class JVMHttpLoader( 10 | callFactoryLazy: Lazy, 11 | ) : HttpLoader { 12 | private val fetcher: HttpUrlFetcher by lazy { HttpUrlFetcher(callFactoryLazy) } 13 | 14 | public override suspend fun get(request: Request.Builder.() -> Unit): HttpResponse { 15 | val requestBuilder = Request.Builder().apply(request).build() 16 | return fetcher 17 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "GET")) 18 | .performAsync() 19 | } 20 | 21 | public override suspend fun post(request: Request.Builder.() -> Unit): HttpResponse { 22 | val requestBuilder = Request.Builder().apply(request).build() 23 | requireNotNull(requestBuilder.body) { "body for method POST should not be null" } 24 | return fetcher 25 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "POST")) 26 | .performAsync() 27 | } 28 | 29 | public override suspend fun put(request: Request.Builder.() -> Unit): HttpResponse { 30 | val requestBuilder = Request.Builder().apply(request).build() 31 | requireNotNull(requestBuilder.body) { "body for method PUT should not be null" } 32 | return fetcher 33 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "PUT")) 34 | .performAsync() 35 | } 36 | 37 | public override suspend fun patch(request: Request.Builder.() -> Unit): HttpResponse { 38 | val requestBuilder = Request.Builder().apply(request).build() 39 | requireNotNull(requestBuilder.body) { "body for method PATCH should not be null" } 40 | return fetcher 41 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "PATCH")) 42 | .performAsync() 43 | } 44 | 45 | public override suspend fun delete(request: Request.Builder.() -> Unit): HttpResponse { 46 | val requestBuilder = Request.Builder().apply(request).build() 47 | return fetcher 48 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "DELETE")) 49 | .performAsync() 50 | } 51 | 52 | public override suspend fun head(request: Request.Builder.() -> Unit): HttpResponse { 53 | val requestBuilder = Request.Builder().apply(request).build() 54 | return fetcher 55 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "HEAD")) 56 | .performAsync() 57 | } 58 | 59 | public override suspend fun sse(request: Request.Builder.() -> Unit): Flow { 60 | val requestBuilder = Request.Builder().apply(request).build() 61 | return fetcher 62 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, "GET")) 63 | .performAsyncWithSSE() 64 | } 65 | 66 | public override suspend fun method(request: Request.Builder.() -> Unit): HttpResponse { 67 | val requestBuilder = Request.Builder().apply(request).build() 68 | val method = requireNotNull(requestBuilder.method) { "method should be not null" } 69 | return fetcher 70 | .fetch(requestBuilder, createRequestBuilder(requestBuilder, method)) 71 | .performAsync() 72 | } 73 | 74 | private fun createRequestBuilder( 75 | request: Request, 76 | method: String, 77 | ): Builder { 78 | val builder = Builder() 79 | with(builder) { 80 | request.headers.forEach { 81 | addHeader(it.key, it.value) 82 | } 83 | 84 | if (HttpMethod.permitsRequestBody(method)) { 85 | method(method, request.body?.toRequestBody()) 86 | } else { 87 | method(method, null) 88 | } 89 | } 90 | return builder 91 | } 92 | 93 | public companion object { 94 | public operator fun invoke(): HttpLoader = FuelBuilder().build() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /plugins/src/main/kotlin/publication.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | plugins { 4 | `maven-publish` 5 | signing 6 | } 7 | 8 | ext["signing.key"] = null 9 | ext["signing.password"] = null 10 | ext["sonatype.username"] = null 11 | ext["sonatype.password"] = null 12 | 13 | val secretPropsFile = project.rootProject.file("local.properties") 14 | if (secretPropsFile.exists()) { 15 | secretPropsFile 16 | .reader() 17 | .use { 18 | Properties().apply { load(it) } 19 | }.onEach { (name, value) -> 20 | ext[name.toString()] = value 21 | } 22 | } else { 23 | ext["signing.key"] = System.getenv("SIGNING_KEY") 24 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD") 25 | ext["sonatype.username"] = System.getenv("SONATYPE_USERNAME") 26 | ext["sonatype.password"] = System.getenv("SONATYPE_PASSWORD") 27 | } 28 | 29 | val javadocJar by tasks.registering(Jar::class) { 30 | archiveClassifier.set("javadoc") 31 | } 32 | 33 | fun getExtraString(name: String) = ext[name]?.toString() 34 | 35 | val isReleaseBuild: Boolean 36 | get() = properties.containsKey("release") 37 | 38 | publishing { 39 | repositories { 40 | maven { 41 | name = "sonatype" 42 | url = 43 | uri( 44 | if (isReleaseBuild) { 45 | "https://oss.sonatype.org/service/local/staging/deploy/maven2" 46 | } else { 47 | "https://s01.oss.sonatype.org/content/repositories/snapshots" 48 | }, 49 | ) 50 | 51 | credentials { 52 | username = getExtraString("sonatype.username") 53 | password = getExtraString("sonatype.password") 54 | } 55 | } 56 | } 57 | 58 | // Creating maven artifacts for jvm 59 | publications { 60 | if (project.name.substringAfterLast("-") == "jvm") { 61 | create("maven") { 62 | from(components["java"]) 63 | } 64 | } 65 | } 66 | 67 | // Configure all publications 68 | publications.withType { 69 | val artifactName: String by project 70 | val artifactDesc: String by project 71 | val artifactUrl: String by project 72 | val artifactScm: String by project 73 | val artifactLicenseName: String by project 74 | val artifactLicenseUrl: String by project 75 | 76 | artifactId = project.name 77 | 78 | if (project.name.substringAfterLast("-") != "jvm") { 79 | artifact(javadocJar) 80 | } 81 | 82 | pom { 83 | name.set(artifactName) 84 | description.set(artifactDesc) 85 | url.set(artifactUrl) 86 | licenses { 87 | license { 88 | name.set(artifactLicenseName) 89 | url.set(artifactLicenseUrl) 90 | distribution.set("repo") 91 | } 92 | } 93 | developers { 94 | developer { 95 | id.set("iNoles") 96 | } 97 | developer { 98 | id.set("kittinunf") 99 | } 100 | } 101 | contributors { 102 | } 103 | scm { 104 | connection.set(artifactScm) 105 | developerConnection.set(artifactScm) 106 | url.set(artifactUrl) 107 | } 108 | } 109 | } 110 | } 111 | 112 | signing { 113 | val signingKey = project.ext["signing.key"] as? String 114 | val signingPassword = project.ext["signing.password"] as? String 115 | if (signingKey == null || signingPassword == null) return@signing 116 | 117 | useInMemoryPgpKeys(signingKey, signingPassword) 118 | sign(publishing.publications) 119 | } 120 | 121 | // TODO: remove after https://youtrack.jetbrains.com/issue/KT-46466 is fixed 122 | project.tasks.withType(AbstractPublishToMaven::class.java).configureEach { 123 | dependsOn(project.tasks.withType(Sign::class.java)) 124 | } 125 | -------------------------------------------------------------------------------- /fuel/src/commonTest/kotlin/fuel/UriCodecTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * Copyright (C) 2022 Eliezer Graber 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package fuel 19 | 20 | import kotlin.test.Test 21 | import kotlin.test.assertEquals 22 | import kotlin.test.fail 23 | 24 | class UriCodecTests { 25 | @Test 26 | fun testDecode_emptyString_returnsEmptyString() { 27 | assertEquals( 28 | "", 29 | UriCodec.decode( 30 | "", 31 | convertPlus = false, 32 | throwOnFailure = true, 33 | ), 34 | ) 35 | } 36 | 37 | @Test 38 | fun testDecode_wrongHexDigit_fails() { 39 | try { 40 | // %p in the end. 41 | UriCodec.decode( 42 | "ab%2f$%C4%82%25%e0%a1%80%p", 43 | convertPlus = false, 44 | throwOnFailure = true, 45 | ) 46 | fail("Expected URISyntaxException") 47 | } catch (expected: IllegalArgumentException) { 48 | // Expected. 49 | } 50 | } 51 | 52 | @Test 53 | fun testDecode_secondHexDigitWrong_fails() { 54 | try { 55 | // %1p in the end. 56 | UriCodec.decode( 57 | "ab%2f$%c4%82%25%e0%a1%80%1p", 58 | convertPlus = false, 59 | throwOnFailure = true, 60 | ) 61 | fail("Expected URISyntaxException") 62 | } catch (expected: IllegalArgumentException) { 63 | // Expected. 64 | } 65 | } 66 | 67 | @Test 68 | fun testDecode_endsWithPercent_fails() { 69 | try { 70 | // % in the end. 71 | UriCodec.decode( 72 | "ab%2f$%c4%82%25%e0%a1%80%", 73 | convertPlus = false, 74 | throwOnFailure = true, 75 | ) 76 | fail("Expected URISyntaxException") 77 | } catch (expected: IllegalArgumentException) { 78 | // Expected. 79 | } 80 | } 81 | 82 | @Test 83 | fun testDecode_dontThrowException_appendsUnknownCharacter() { 84 | assertEquals( 85 | "ab/$\u0102%\u0840\ufffd", 86 | UriCodec.decode( 87 | "ab%2f$%c4%82%25%e0%a1%80%", 88 | convertPlus = false, 89 | throwOnFailure = false, 90 | ), 91 | ) 92 | } 93 | 94 | @Test 95 | fun testDecode_convertPlus() { 96 | assertEquals( 97 | "ab/$\u0102% \u0840", 98 | UriCodec.decode( 99 | "ab%2f$%c4%82%25+%e0%a1%80", 100 | convertPlus = true, 101 | throwOnFailure = false, 102 | ), 103 | ) 104 | } 105 | 106 | // Last character needs decoding (make sure we are flushing the buffer with chars to decode). 107 | @Test 108 | fun testDecode_lastCharacter() { 109 | assertEquals( 110 | "ab/$\u0102%\u0840", 111 | UriCodec.decode( 112 | "ab%2f$%c4%82%25%e0%a1%80", 113 | convertPlus = false, 114 | throwOnFailure = true, 115 | ), 116 | ) 117 | } 118 | 119 | // Check that a second row of encoded characters is decoded properly (internal buffers are 120 | // reset properly). 121 | @Test 122 | fun testDecode_secondRowOfEncoded() { 123 | assertEquals( 124 | "ab/$\u0102%\u0840aa\u0840", 125 | UriCodec.decode( 126 | "ab%2f$%c4%82%25%e0%a1%80aa%e0%a1%80", 127 | convertPlus = false, 128 | throwOnFailure = true, 129 | ), 130 | ) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /samples/httpbin-wasm/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kotlin/Wasm Example 7 | 8 | 62 | 63 | 64 | 65 | 66 | 67 |
68 | ⚠️ Please make sure that your runtime environment supports the latest version of Wasm GC and Exception-Handling proposals. 69 | For more information, see https://kotl.in/wasm-help. 70 |
71 |
72 |
    73 |
  • For Chrome and Chromium-based browsers (Edge, Brave etc.), it should just work since version 119.
  • 74 |
  • For Firefox 120 it should just work.
  • 75 |
  • For Firefox 119: 76 |
      77 |
    1. Open about:config in the browser.
    2. 78 |
    3. Enable javascript.options.wasm_gc.
    4. 79 |
    5. Refresh this page.
    6. 80 |
    81 |
  • 82 |
83 |
84 | 100 | 101 | 102 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuel 2 | 3 | [![Kotlin](https://img.shields.io/badge/Kotlin-2.0-blue.svg)](http://kotlinlang.org) 4 | [![MavenCentral](https://maven-badges.herokuapp.com/maven-central/com.github.kittinunf.fuel/fuel-jvm/badge.svg)](https://search.maven.org/search?q=com.github.kittinunf.fuel) 5 | [![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io) 6 | [![Run Gradle Push](https://github.com/kittinunf/fuel/actions/workflows/main.yml/badge.svg)](https://github.com/kittinunf/fuel/actions/workflows/main.yml) 7 | [![Codecov](https://codecov.io/github/kittinunf/fuel/coverage.svg?branch=main)](https://codecov.io/gh/kittinunf/fuel/branch/main) 8 | 9 | The easiest HTTP networking library for Kotlin backed by Kotlinx Coroutines. 10 | 11 | ## Migration 12 | 13 | From 3.x onwards, we are using [main](https://github.com/kittinunf/fuel/tree/main) as our new base branch. If you are finding the old version [2.x](https://github.com/kittinunf/fuel/tree/2.x), please take a look at our old branch. 14 | 15 | ## Download 16 | 17 | ### For release version 18 | 19 | ```kotlin 20 | implementation("com.github.kittinunf.fuel:fuel:3.0.0-alpha04") 21 | ``` 22 | 23 | ## Quick Start 24 | 25 | use the `any http method` [suspend](https://kotlinlang.org/docs/reference/coroutines/basics.html) function: 26 | 27 | ```kotlin 28 | runBlocking { 29 | val string: String = Fuel.get("https://publicobject.com/helloworld.txt").body.string() 30 | println(string) 31 | } 32 | 33 | runBlocking { 34 | val string: String = "https://publicobject.com/helloworld.txt".httpGet().body.string() 35 | println(string) 36 | } 37 | 38 | runBlocking { 39 | val fuel = FuelBuilder().build() 40 | val string: String = fuel.get(request = { url = "https://publicobject.com/helloworld.txt" }).body.string() 41 | println(string) 42 | } 43 | 44 | ``` 45 | 46 | ## Custom Configuration 47 | 48 | JVM uses [OkHttpClient](https://square.github.io/okhttp/4.x/okhttp/okhttp3/-ok-http-client/) configurations 49 | 50 | ```kotlin 51 | val fuel = FuelBuilder().config(OKHttpClient()).build() 52 | val string = fuel.get(request = { url = "https://publicobject.com/helloworld.txt" }).body.string() 53 | ``` 54 | 55 | Apple uses [NSURLSessionConfiguration](https://developer.apple.com/documentation/foundation/nsurlsessionconfiguration) 56 | 57 | ```kotlin 58 | val fuel = FuelBuilder().config(NSURLSessionConfiguration.defaultSessionConfiguration).build() 59 | val string = fuel.get(request = { url = "https://publicobject.com/helloworld.txt" }).body.string() 60 | ``` 61 | 62 | Please note it will throw Exceptions. Make sure you catch it on the production apps. 63 | 64 | Fuel requires Java 8 byte code. 65 | 66 | ## Requirements 67 | 68 | - If you are using Android, It needs to be Android 5+. 69 | - Java 8+ 70 | 71 | ## R8 / Proguard 72 | 73 | Fuel is fully compatible with R8 out of the box and doesn't require adding any extra rules. 74 | 75 | If you use Proguard, you may need to add rules for [Coroutines](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro), [OkHttp](https://github.com/square/okhttp/blob/master/okhttp/src/main/resources/META-INF/proguard/okhttp3.pro) and [Okio](https://github.com/square/okio/blob/master/okio/src/jvmMain/resources/META-INF/proguard/okio.pro). 76 | 77 | If you use the fuel-serialization modules, you may need to add rules for [Serialization](https://github.com/Kotlin/kotlinx.serialization#androidjvm). 78 | 79 | If you use the fuel-moshi modules, you may need to add rules for [Moshi](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro) and [Moshi-Kotlin](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro) 80 | 81 | ## Other libraries 82 | 83 | If you like Fuel, you might also like other libraries of mine; 84 | 85 | - [Result](https://github.com/kittinunf/Result) - The modelling for success/failure of operations in Kotlin 86 | - [Fuse](https://github.com/kittinunf/Fuse) - A simple generic LRU memory/disk cache for Android written in Kotlin 87 | - [Forge](https://github.com/kittinunf/Forge) - Functional style JSON parsing written in Kotlin 88 | - [ReactiveAndroid](https://github.com/kittinunf/ReactiveAndroid) - Reactive events and properties with RxJava for Android SDK 89 | 90 | ## Credits 91 | 92 | Fuel brought to you by [contributors](https://github.com/kittinunf/Fuel/graphs/contributors). 93 | 94 | ## Licenses 95 | 96 | Fuel released under the [MIT](https://opensource.org/licenses/MIT) license. 97 | -------------------------------------------------------------------------------- /fuel/src/jvmTest/kotlin/fuel/HttpLoaderBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.io.readString 5 | import mockwebserver3.MockResponse 6 | import mockwebserver3.MockWebServer 7 | import mockwebserver3.SocketPolicy 8 | import okhttp3.ExperimentalOkHttpApi 9 | import okhttp3.OkHttpClient 10 | import org.junit.After 11 | import org.junit.Assert.assertEquals 12 | import org.junit.Before 13 | import org.junit.Test 14 | import java.net.SocketTimeoutException 15 | import java.util.concurrent.TimeUnit 16 | import kotlin.test.assertNotNull 17 | 18 | @OptIn(ExperimentalOkHttpApi::class) 19 | class HttpLoaderBuilderTest { 20 | private lateinit var mockWebServer: MockWebServer 21 | 22 | @Before 23 | fun `before test`() { 24 | mockWebServer = MockWebServer().apply { start() } 25 | } 26 | 27 | @After 28 | fun `after test`() { 29 | mockWebServer.shutdown() 30 | } 31 | 32 | @Test 33 | fun `default okhttp settings`() = 34 | runBlocking { 35 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 36 | val response = 37 | JVMHttpLoader() 38 | .get { 39 | url = mockWebServer.url("hello").toString() 40 | }.source 41 | .readString() 42 | assertEquals("Hello World", response) 43 | 44 | mockWebServer.shutdown() 45 | } 46 | 47 | @Test 48 | fun `default okhttp settings with parameter`() = 49 | runBlocking { 50 | mockWebServer.enqueue(MockResponse(body = "Hello World 3")) 51 | val response = 52 | JVMHttpLoader() 53 | .get { 54 | url = mockWebServer.url("hello").toString() 55 | parameters = listOf("foo" to "bar") 56 | }.source 57 | .readString() 58 | assertEquals("Hello World 3", response) 59 | 60 | mockWebServer.shutdown() 61 | } 62 | 63 | @Test 64 | fun `default okhttp settings with headers`() = 65 | runBlocking { 66 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 67 | val response = 68 | JVMHttpLoader() 69 | .get { 70 | url = mockWebServer.url("hello").toString() 71 | }.headers 72 | assertEquals("11", response["Content-Length"]) 73 | mockWebServer.shutdown() 74 | } 75 | 76 | @Test 77 | fun `setting connect timeouts`() = 78 | runBlocking { 79 | mockWebServer.enqueue(MockResponse(body = "Hello World 4")) 80 | 81 | val httpLoader = 82 | FuelBuilder() 83 | .config { 84 | OkHttpClient.Builder().connectTimeout(30L, TimeUnit.MILLISECONDS).build() 85 | }.build() 86 | val response = 87 | httpLoader 88 | .get { 89 | url = mockWebServer.url("hello").toString() 90 | }.source 91 | .readString() 92 | assertEquals("Hello World 4", response) 93 | } 94 | 95 | @Test 96 | fun `setting call timeouts`() = 97 | runBlocking { 98 | mockWebServer.enqueue(MockResponse(body = "Hello World 5")) 99 | 100 | val okhttp = OkHttpClient.Builder().callTimeout(30L, TimeUnit.MILLISECONDS).build() 101 | val httpLoader = FuelBuilder().config(okhttp).build() 102 | val response = 103 | httpLoader 104 | .get { 105 | url = mockWebServer.url("hello2").toString() 106 | }.source 107 | .readString() 108 | assertEquals("Hello World 5", response) 109 | } 110 | 111 | @Test 112 | fun `no socket response`(): Unit = 113 | runBlocking { 114 | mockWebServer.enqueue(MockResponse(body = "{}", socketPolicy = SocketPolicy.NoResponse)) 115 | try { 116 | JVMHttpLoader() 117 | .get { 118 | url = mockWebServer.url("socket").toString() 119 | }.source 120 | .readString() 121 | } catch (ste: SocketTimeoutException) { 122 | assertNotNull(ste, "socket timeout") 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /fuel-moshi-jvm/src/test/kotlin/fuel/moshi/FuelMoshiTest.kt: -------------------------------------------------------------------------------- 1 | package fuel.moshi 2 | 3 | import com.squareup.moshi.JsonClass 4 | import com.squareup.moshi.Types 5 | import fuel.Fuel 6 | import fuel.get 7 | import kotlinx.coroutines.runBlocking 8 | import mockwebserver3.MockResponse 9 | import mockwebserver3.MockWebServer 10 | import okhttp3.ExperimentalOkHttpApi 11 | import org.junit.Assert.assertEquals 12 | import org.junit.Assert.fail 13 | import org.junit.Test 14 | 15 | @OptIn(ExperimentalOkHttpApi::class) 16 | class FuelMoshiTest { 17 | @JsonClass(generateAdapter = true) 18 | data class HttpBinUserAgentModel( 19 | var userAgent: String = "", 20 | ) 21 | 22 | @JsonClass(generateAdapter = true) 23 | data class Card( 24 | val rank: Char, 25 | val suit: String, 26 | ) 27 | 28 | @Test 29 | fun testMoshiResponse(): Unit = 30 | runBlocking { 31 | val mockWebServer = startMockServerWithBody("{\"userAgent\": \"Fuel\"}") 32 | 33 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 34 | val moshi = response.toMoshi(HttpBinUserAgentModel::class.java) 35 | moshi.fold({ 36 | assertEquals("Fuel", it?.userAgent) 37 | }, { 38 | fail(it.localizedMessage) 39 | }) 40 | 41 | mockWebServer.shutdown() 42 | } 43 | 44 | @Test 45 | fun testReifiedTypeMoshiResponse(): Unit = 46 | runBlocking { 47 | val mockWebServer = startMockServerWithBody("{\"userAgent\": \"Fuel\"}") 48 | 49 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 50 | val moshi = response.toMoshi() 51 | moshi.fold({ 52 | assertEquals("Fuel", it?.userAgent) 53 | }, { 54 | fail(it.localizedMessage) 55 | }) 56 | 57 | mockWebServer.shutdown() 58 | } 59 | 60 | @Test 61 | fun testMoshiGenericList() = 62 | runBlocking { 63 | val mockWebServer = 64 | startMockServerWithBody( 65 | "[{ " + 66 | " \"rank\": \"4\"," + 67 | " \"suit\": \"CLUBS\"" + 68 | " }, {" + 69 | " \"rank\": \"A\"," + 70 | " \"suit\": \"HEARTS\"" + 71 | " }, {" + 72 | " \"rank\": \"J\"," + 73 | " \"suit\": \"SPADES\"" + 74 | " }" + 75 | "]", 76 | ) 77 | 78 | val response = Fuel.get(mockWebServer.url("user-agent").toString()) 79 | val listOfCardsType = Types.newParameterizedType(List::class.java, Card::class.java) 80 | val cards = response.toMoshi>(listOfCardsType) 81 | cards.fold({ 82 | assertEquals(3, it?.size) 83 | assertEquals("CLUBS", it?.get(0)?.suit) 84 | }, { 85 | fail(it.localizedMessage) 86 | }) 87 | 88 | mockWebServer.shutdown() 89 | } 90 | 91 | @Test 92 | fun customMoshiAdapterWithGenericList() = 93 | runBlocking { 94 | val mockWebServer = 95 | startMockServerWithBody( 96 | "[{" + 97 | " \"rank\": \"1\"," + 98 | " \"suit\": \"CLUBS\"" + 99 | " }, {" + 100 | " \"rank\": \"J\"," + 101 | " \"suit\": \"HEARTS\"" + 102 | " }, {" + 103 | " \"rank\": \"K\"," + 104 | " \"suit\": \"SPADES\"" + 105 | " }" + 106 | "]", 107 | ) 108 | 109 | val userAgentResponse = Fuel.get(mockWebServer.url("user-agent").toString()) 110 | val listOfCardsType = Types.newParameterizedType(List::class.java, Card::class.java) 111 | val adapter = defaultMoshi.build().adapter>(listOfCardsType) 112 | val cards = userAgentResponse.toMoshi>(adapter) 113 | cards.fold({ 114 | assertEquals(3, it?.size) 115 | assertEquals("CLUBS", it?.get(0)?.suit) 116 | }, { 117 | fail(it.localizedMessage) 118 | }) 119 | 120 | mockWebServer.shutdown() 121 | } 122 | 123 | private fun startMockServerWithBody(body: String): MockWebServer = 124 | MockWebServer().apply { 125 | enqueue(MockResponse(body = body)) 126 | start() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /fuel/src/jvmTest/kotlin/fuel/StringsTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.io.readString 5 | import mockwebserver3.MockResponse 6 | import mockwebserver3.MockWebServer 7 | import okhttp3.ExperimentalOkHttpApi 8 | import okhttp3.OkHttpClient 9 | import org.junit.After 10 | import org.junit.Assert.assertEquals 11 | import org.junit.Before 12 | import org.junit.Test 13 | 14 | @OptIn(ExperimentalOkHttpApi::class) 15 | class StringsTest { 16 | private lateinit var mockWebServer: MockWebServer 17 | 18 | @Before 19 | fun `before test`() { 20 | Fuel.setHttpLoader(JVMHttpLoader(lazyOf(OkHttpClient()))) 21 | mockWebServer = MockWebServer().apply { start() } 22 | } 23 | 24 | @After 25 | fun `after test`() { 26 | mockWebServer.shutdown() 27 | } 28 | 29 | @Test 30 | fun `get test data`() = 31 | runBlocking { 32 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 33 | 34 | val string = 35 | mockWebServer 36 | .url("get") 37 | .toString() 38 | .httpGet() 39 | .source 40 | .readString() 41 | val request2 = mockWebServer.takeRequest() 42 | 43 | assertEquals("GET", request2.method) 44 | assertEquals(string, "Hello World") 45 | } 46 | 47 | @Test 48 | fun `post test data`() = 49 | runBlocking { 50 | mockWebServer.enqueue(MockResponse()) 51 | 52 | mockWebServer.url("post").toString().httpPost(body = "Hi?") 53 | val request1 = mockWebServer.takeRequest() 54 | 55 | assertEquals("POST", request1.method) 56 | val utf8 = request1.body.readUtf8() 57 | assertEquals("Hi?", utf8) 58 | } 59 | 60 | @Test(expected = IllegalArgumentException::class) 61 | fun `empty response body for post`() = 62 | runBlocking { 63 | mockWebServer.enqueue(MockResponse()) 64 | 65 | mockWebServer.url("post").toString().httpPost() 66 | val request1 = mockWebServer.takeRequest() 67 | 68 | assertEquals("POST", request1.method) 69 | } 70 | 71 | @Test 72 | fun `put test data`() = 73 | runBlocking { 74 | mockWebServer.enqueue(MockResponse()) 75 | 76 | mockWebServer.url("put").toString().httpPut(body = "Put There") 77 | val request1 = mockWebServer.takeRequest() 78 | 79 | assertEquals("PUT", request1.method) 80 | val utf8 = request1.body.readUtf8() 81 | assertEquals("Put There", utf8) 82 | } 83 | 84 | @Test(expected = IllegalArgumentException::class) 85 | fun `empty response body for put`() = 86 | runBlocking { 87 | mockWebServer.enqueue(MockResponse()) 88 | 89 | mockWebServer.url("put").toString().httpPut() 90 | val request1 = mockWebServer.takeRequest() 91 | 92 | assertEquals("PUT", request1.method) 93 | } 94 | 95 | @Test 96 | fun `patch test data`() = 97 | runBlocking { 98 | mockWebServer.enqueue(MockResponse()) 99 | 100 | mockWebServer.url("patch").toString().httpPatch(body = "Hello There") 101 | val request1 = mockWebServer.takeRequest() 102 | 103 | assertEquals("PATCH", request1.method) 104 | val utf8 = request1.body.readUtf8() 105 | assertEquals("Hello There", utf8) 106 | } 107 | 108 | @Test(expected = IllegalArgumentException::class) 109 | fun `empty response body for patch`() = 110 | runBlocking { 111 | mockWebServer.enqueue(MockResponse()) 112 | 113 | mockWebServer.url("patch").toString().httpPatch() 114 | val request1 = mockWebServer.takeRequest() 115 | 116 | assertEquals("PATCH", request1.method) 117 | } 118 | 119 | @Test 120 | fun `delete test data`() = 121 | runBlocking { 122 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 123 | 124 | val string = 125 | mockWebServer 126 | .url("delete") 127 | .toString() 128 | .httpDelete() 129 | .source 130 | .readString() 131 | val request1 = mockWebServer.takeRequest() 132 | 133 | assertEquals("DELETE", request1.method) 134 | assertEquals(string, "Hello World") 135 | } 136 | 137 | @Test 138 | fun `head test data`() = 139 | runBlocking { 140 | mockWebServer.enqueue(MockResponse()) 141 | 142 | mockWebServer.url("head").toString().httpHead() 143 | val request1 = mockWebServer.takeRequest() 144 | 145 | assertEquals("HEAD", request1.method) 146 | } 147 | 148 | @Test 149 | fun `connect test data`() = 150 | runBlocking { 151 | mockWebServer.enqueue(MockResponse()) 152 | 153 | mockWebServer.url("connect").toString().httpMethod(method = "CONNECT") 154 | val request1 = mockWebServer.takeRequest() 155 | 156 | assertEquals("CONNECT", request1.method) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /fuel/src/appleMain/kotlin/fuel/HttpUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.cinterop.BetaInteropApi 4 | import kotlinx.coroutines.channels.awaitClose 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.callbackFlow 7 | import kotlinx.coroutines.suspendCancellableCoroutine 8 | import kotlinx.io.Buffer 9 | import platform.Foundation.NSData 10 | import platform.Foundation.NSHTTPURLResponse 11 | import platform.Foundation.NSMutableURLRequest 12 | import platform.Foundation.NSURL 13 | import platform.Foundation.NSURLRequestReloadIgnoringCacheData 14 | import platform.Foundation.NSURLSession 15 | import platform.Foundation.NSURLSessionConfiguration 16 | import platform.Foundation.dataTaskWithRequest 17 | import platform.Foundation.setHTTPBody 18 | import platform.Foundation.setHTTPMethod 19 | import platform.Foundation.setValue 20 | import kotlin.collections.forEach 21 | import kotlin.coroutines.resume 22 | 23 | internal class HttpUrlFetcher( 24 | private val sessionConfiguration: NSURLSessionConfiguration, 25 | ) { 26 | @OptIn(BetaInteropApi::class) 27 | suspend fun fetch( 28 | method: String, 29 | request: Request, 30 | ): HttpResponse = 31 | suspendCancellableCoroutine { continuation -> 32 | val url = 33 | request.parameters?.let { 34 | request.url.fillURLWithParameters(it) 35 | } ?: request.url 36 | 37 | val mutableURLRequest = 38 | NSMutableURLRequest.requestWithURL(NSURL(string = url)).apply { 39 | request.body?.let { 40 | setHTTPBody(it.encode()) 41 | } 42 | request.headers.forEach { 43 | setValue(it.value, forHTTPHeaderField = it.key) 44 | } 45 | setCachePolicy(NSURLRequestReloadIgnoringCacheData) 46 | setHTTPMethod(method) 47 | } 48 | 49 | val session = NSURLSession.sessionWithConfiguration(sessionConfiguration) 50 | val task = 51 | session.dataTaskWithRequest(mutableURLRequest) { httpData, nsUrlResponse, error -> 52 | if (error != null) { 53 | continuation.resumeWith(Result.failure(Throwable(error.localizedDescription))) 54 | return@dataTaskWithRequest 55 | } 56 | 57 | val httpResponse = nsUrlResponse as? NSHTTPURLResponse 58 | if (httpResponse == null) { 59 | continuation.resumeWith(Result.failure(Throwable("Invalid HTTP response"))) 60 | return@dataTaskWithRequest 61 | } 62 | 63 | continuation.resume(buildHttpResponse(httpData, httpResponse)) 64 | } 65 | continuation.invokeOnCancellation { task.cancel() } 66 | task.resume() 67 | } 68 | 69 | fun fetchSSE(request: Request): Flow = 70 | callbackFlow { 71 | val url = 72 | request.parameters?.let { 73 | request.url.fillURLWithParameters(it) 74 | } ?: request.url 75 | val mutableURLRequest = 76 | NSMutableURLRequest.requestWithURL(NSURL(string = url)).apply { 77 | setHTTPMethod("GET") 78 | setValue("text/event-stream", forHTTPHeaderField = "Accept") 79 | request.headers.forEach { setValue(it.value, forHTTPHeaderField = it.key) } 80 | } 81 | 82 | val session = NSURLSession.sessionWithConfiguration(sessionConfiguration) 83 | val task = 84 | session.dataTaskWithRequest(mutableURLRequest) { httpData, nsUrlResponse, error -> 85 | if (error != null) { 86 | close(Throwable(error.localizedDescription)) 87 | return@dataTaskWithRequest 88 | } 89 | 90 | val httpResponse = nsUrlResponse as? NSHTTPURLResponse 91 | if (httpResponse?.statusCode?.toInt() != 200) { 92 | close(Throwable("Unexpected status code: ${httpResponse?.statusCode}")) 93 | return@dataTaskWithRequest 94 | } 95 | 96 | httpData?.toByteArray()?.decodeToString()?.lines()?.forEach { line -> 97 | if (line.startsWith("data: ")) { 98 | trySend(line.removePrefix("data: ").trim()) // Send each event 99 | } 100 | } 101 | } 102 | 103 | awaitClose { task.cancel() } // Cancel task if flow is closed 104 | task.resume() 105 | } 106 | 107 | private fun buildHttpResponse( 108 | data: NSData?, 109 | httpResponse: NSHTTPURLResponse?, 110 | ): HttpResponse { 111 | if (httpResponse == null) { 112 | throw Throwable("Failed to parse HTTP network response: EOF") 113 | } 114 | 115 | val sourceBuffer = Buffer() 116 | sourceBuffer.write(data?.toByteArray() ?: ByteArray(0)) 117 | 118 | return HttpResponse().apply { 119 | statusCode = httpResponse.statusCode.toInt() 120 | source = sourceBuffer 121 | headers = httpResponse.readHeaders() 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /fuel/src/jvmTest/kotlin/fuel/HttpLoaderTest.kt: -------------------------------------------------------------------------------- 1 | package fuel 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import kotlinx.io.readString 5 | import mockwebserver3.MockResponse 6 | import mockwebserver3.MockWebServer 7 | import okhttp3.ExperimentalOkHttpApi 8 | import okhttp3.OkHttpClient 9 | import org.junit.After 10 | import org.junit.Assert.assertEquals 11 | import org.junit.Before 12 | import org.junit.Test 13 | 14 | @OptIn(ExperimentalOkHttpApi::class) 15 | class HttpLoaderTest { 16 | private lateinit var httpLoader: HttpLoader 17 | private lateinit var mockWebServer: MockWebServer 18 | 19 | @Before 20 | fun `before test`() { 21 | httpLoader = JVMHttpLoader(lazyOf(OkHttpClient())) 22 | mockWebServer = MockWebServer().apply { start() } 23 | } 24 | 25 | @After 26 | fun `after test`() { 27 | mockWebServer.shutdown() 28 | } 29 | 30 | @Test 31 | fun `unsuccessful 404 Error`() = 32 | runBlocking { 33 | mockWebServer.enqueue(MockResponse(code = 404, body = "Hello World")) 34 | 35 | val string = 36 | httpLoader 37 | .get { 38 | url = mockWebServer.url("get").toString() 39 | }.source 40 | .readString() 41 | 42 | val request1 = mockWebServer.takeRequest() 43 | 44 | assertEquals("GET", request1.method) 45 | assertEquals(string, "Hello World") 46 | } 47 | 48 | @Test 49 | fun `get test data`() = 50 | runBlocking { 51 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 52 | 53 | val string = 54 | httpLoader 55 | .get { 56 | url = mockWebServer.url("get").toString() 57 | }.source 58 | .readString() 59 | 60 | val request2 = mockWebServer.takeRequest() 61 | 62 | assertEquals("GET", request2.method) 63 | assertEquals(string, "Hello World") 64 | } 65 | 66 | @Test 67 | fun `get test data with parameters`() = 68 | runBlocking { 69 | mockWebServer.enqueue(MockResponse(body = "Hello There")) 70 | 71 | val string = 72 | httpLoader 73 | .get { 74 | url = mockWebServer.url("get").toString() 75 | parameters = listOf("foo" to "bar") 76 | }.source 77 | .readString() 78 | 79 | val request1 = mockWebServer.takeRequest() 80 | 81 | assertEquals("GET", request1.method) 82 | assertEquals(string, "Hello There") 83 | } 84 | 85 | @Test 86 | fun `get test data with headers`() = 87 | runBlocking { 88 | mockWebServer.enqueue(MockResponse(body = "Greeting Everyone")) 89 | 90 | val string = 91 | httpLoader 92 | .get { 93 | url = mockWebServer.url("get").toString() 94 | headers = mapOf("foo" to "bar") 95 | }.source 96 | .readString() 97 | 98 | val request1 = mockWebServer.takeRequest() 99 | 100 | assertEquals("GET", request1.method) 101 | assertEquals(string, "Greeting Everyone") 102 | } 103 | 104 | @Test 105 | fun `post test data`() = 106 | runBlocking { 107 | mockWebServer.enqueue(MockResponse()) 108 | 109 | httpLoader.post { 110 | url = mockWebServer.url("post").toString() 111 | body = "Hi?" 112 | } 113 | val request1 = mockWebServer.takeRequest() 114 | 115 | assertEquals("POST", request1.method) 116 | val utf8 = request1.body.readUtf8() 117 | assertEquals("Hi?", utf8) 118 | } 119 | 120 | @Test(expected = IllegalArgumentException::class) 121 | fun `empty response body for post`() = 122 | runBlocking { 123 | mockWebServer.enqueue(MockResponse()) 124 | 125 | httpLoader.post { 126 | url = mockWebServer.url("post").toString() 127 | } 128 | 129 | val request1 = mockWebServer.takeRequest() 130 | assertEquals("POST", request1.method) 131 | } 132 | 133 | @Test 134 | fun `put test data`() = 135 | runBlocking { 136 | mockWebServer.enqueue(MockResponse()) 137 | 138 | httpLoader.put { 139 | url = mockWebServer.url("put").toString() 140 | body = "Put There" 141 | } 142 | 143 | val request1 = mockWebServer.takeRequest() 144 | 145 | assertEquals("PUT", request1.method) 146 | val utf8 = request1.body.readUtf8() 147 | assertEquals("Put There", utf8) 148 | } 149 | 150 | @Test(expected = IllegalArgumentException::class) 151 | fun `empty response body for put`() = 152 | runBlocking { 153 | mockWebServer.enqueue(MockResponse()) 154 | 155 | httpLoader.put { 156 | url = mockWebServer.url("put").toString() 157 | } 158 | 159 | val request1 = mockWebServer.takeRequest() 160 | 161 | assertEquals("PUT", request1.method) 162 | } 163 | 164 | @Test 165 | fun `patch test data`() = 166 | runBlocking { 167 | mockWebServer.enqueue(MockResponse()) 168 | 169 | httpLoader.patch { 170 | url = mockWebServer.url("patch").toString() 171 | body = "Hello There" 172 | } 173 | 174 | val request1 = mockWebServer.takeRequest() 175 | 176 | assertEquals("PATCH", request1.method) 177 | val utf8 = request1.body.readUtf8() 178 | assertEquals("Hello There", utf8) 179 | } 180 | 181 | @Test(expected = IllegalArgumentException::class) 182 | fun `empty response body for patch`() = 183 | runBlocking { 184 | mockWebServer.enqueue(MockResponse()) 185 | 186 | httpLoader.patch { 187 | url = mockWebServer.url("patch").toString() 188 | } 189 | 190 | val request1 = mockWebServer.takeRequest() 191 | 192 | assertEquals("PATCH", request1.method) 193 | } 194 | 195 | @Test 196 | fun `delete test data`() = 197 | runBlocking { 198 | mockWebServer.enqueue(MockResponse(body = "Hello World")) 199 | 200 | val string = 201 | httpLoader 202 | .delete { 203 | url = mockWebServer.url("delete").toString() 204 | }.source 205 | .readString() 206 | 207 | val request1 = mockWebServer.takeRequest() 208 | 209 | assertEquals("DELETE", request1.method) 210 | assertEquals(string, "Hello World") 211 | } 212 | 213 | @Test 214 | fun `head test data`() = 215 | runBlocking { 216 | mockWebServer.enqueue(MockResponse()) 217 | 218 | httpLoader.head { 219 | url = mockWebServer.url("head").toString() 220 | } 221 | 222 | val request1 = mockWebServer.takeRequest() 223 | 224 | assertEquals("HEAD", request1.method) 225 | } 226 | 227 | @Test 228 | fun `connect test data`() = 229 | runBlocking { 230 | mockWebServer.enqueue(MockResponse()) 231 | 232 | httpLoader.method { 233 | url = mockWebServer.url("connect").toString() 234 | method = "CONNECT" 235 | } 236 | 237 | val request1 = mockWebServer.takeRequest() 238 | 239 | assertEquals("CONNECT", request1.method) 240 | } 241 | 242 | @Test(expected = IllegalArgumentException::class) 243 | fun `empty method for CONNECT`() = 244 | runBlocking { 245 | mockWebServer.enqueue(MockResponse()) 246 | 247 | httpLoader.method { 248 | url = mockWebServer.url("connect").toString() 249 | } 250 | 251 | val request1 = mockWebServer.takeRequest() 252 | 253 | assertEquals("CONNECT", request1.method) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /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 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /fuel/src/commonMain/kotlin/fuel/UriCodec.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2007 The Android Open Source Project 3 | * Copyright (C) 2022 Eliezer Graber 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package fuel 19 | 20 | public object UriCodec { 21 | /** 22 | * Encodes characters in the given string as '%'-escaped octets 23 | * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 24 | * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 25 | * all other characters. 26 | * 27 | * @param s string to encode 28 | * @return an encoded version of s suitable for use as a URI component, 29 | * or null if s is null 30 | */ 31 | public fun encodeOrNull(s: String?): String? = if (s == null) null else encode(s, null) 32 | 33 | /** 34 | * Encodes characters in the given string as '%'-escaped octets 35 | * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 36 | * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 37 | * all other characters with the exception of those specified in the 38 | * allow argument. 39 | * 40 | * @param s string to encode 41 | * @param allow set of additional characters to allow in the encoded form, 42 | * null if no characters should be skipped 43 | * @return an encoded version of s suitable for use as a URI component, 44 | * or null if s is null 45 | */ 46 | public fun encodeOrNull( 47 | s: String?, 48 | allow: String?, 49 | ): String? = if (s == null) null else encode(s, allow) 50 | 51 | /** 52 | * Encodes characters in the given string as '%'-escaped octets 53 | * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 54 | * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 55 | * all other characters. 56 | * 57 | * @param s string to encode 58 | * @return an encoded version of s suitable for use as a URI component, 59 | * or null if s is null 60 | */ 61 | public fun encode(s: String): String = encode(s, null) 62 | 63 | /** 64 | * Encodes characters in the given string as '%'-escaped octets 65 | * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers 66 | * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes 67 | * all other characters with the exception of those specified in the 68 | * allow argument. 69 | * 70 | * @param s string to encode 71 | * @param allow set of additional characters to allow in the encoded form, 72 | * null if no characters should be skipped 73 | * @return an encoded version of s suitable for use as a URI component 74 | */ 75 | public fun encode( 76 | s: String, 77 | allow: String?, 78 | ): String { 79 | // Lazily-initialized buffers. 80 | var encoded: StringBuilder? = null 81 | 82 | val oldLength: Int = s.length 83 | 84 | // This loop alternates between copying over allowed characters and 85 | // encoding in chunks. This results in fewer method calls and 86 | // allocations than encoding one character at a time. 87 | var current = 0 88 | while (current < oldLength) { 89 | // Start in "copying" mode where we copy over allowed chars. 90 | 91 | // Find the next character which needs to be encoded. 92 | var nextToEncode = current 93 | while (nextToEncode < oldLength && isAllowed(s[nextToEncode], allow)) { 94 | nextToEncode++ 95 | } 96 | 97 | // If there's nothing more to encode... 98 | if (nextToEncode == oldLength) { 99 | return if (current == 0) { 100 | // We didn't need to encode anything! 101 | s 102 | } else { 103 | // Presumably, we've already done some encoding. 104 | encoded!!.append(s, current, oldLength) 105 | encoded.toString() 106 | } 107 | } 108 | 109 | if (encoded == null) { 110 | encoded = StringBuilder() 111 | } 112 | 113 | if (nextToEncode > current) { 114 | // Append allowed characters leading up to this point. 115 | encoded.append(s, current, nextToEncode) 116 | } else { 117 | // assert nextToEncode == current 118 | } 119 | 120 | // Switch to "encoding" mode. 121 | 122 | // Find the next allowed character. 123 | current = nextToEncode 124 | var nextAllowed = current + 1 125 | while (nextAllowed < oldLength && !isAllowed(s[nextAllowed], allow)) { 126 | nextAllowed++ 127 | } 128 | 129 | // Convert the substring to bytes and encode the bytes as 130 | // '%'-escaped octets. 131 | val toEncode = s.substring(current, nextAllowed) 132 | try { 133 | val bytes: ByteArray = toEncode.encodeToByteArray() 134 | val bytesLength = bytes.size 135 | for (i in 0 until bytesLength) { 136 | encoded.append('%') 137 | encoded.append(hexDigits[bytes[i].toInt() and 0xf0 shr 4]) 138 | encoded.append(hexDigits[bytes[i].toInt() and 0xf]) 139 | } 140 | } catch (e: Exception) { 141 | throw AssertionError(e) 142 | } 143 | current = nextAllowed 144 | } 145 | 146 | ByteArray(0).decodeToString() 147 | 148 | // Encoded could still be null at this point if s is empty. 149 | return encoded?.toString() ?: s 150 | } 151 | 152 | /** 153 | * Returns true if the given character is allowed. 154 | * 155 | * @param c character to check 156 | * @param allow characters to allow 157 | * @return true if the character is allowed or false if it should be 158 | * encoded 159 | */ 160 | private fun isAllowed( 161 | c: Char, 162 | allow: String?, 163 | ): Boolean = 164 | c in lowercaseAsciiAlphaRange || 165 | c in uppercaseAsciiAlphaRange || 166 | c in digitAsciiRange || 167 | c in defaultAllowedSet || 168 | allow != null && 169 | allow.indexOf(c) != -1 170 | 171 | /** 172 | * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. 173 | * Replaces invalid octets with the unicode replacement character 174 | * ("\\uFFFD"). 175 | * 176 | * @param s encoded string to decode 177 | * @param convertPlus if `convertPlus == true` all ‘+’ chars in the decoded output are converted to ‘ ‘ 178 | * (white space) 179 | * @param throwOnFailure if `throwOnFailure == true` an [IllegalArgumentException] is thrown for 180 | * invalid inputs. Else, U+FFd is emitted to the output in place of invalid input octets. 181 | * @return the given string with escaped octets decoded, or null if s is null 182 | */ 183 | public fun decodeOrNull( 184 | s: String?, 185 | convertPlus: Boolean = false, 186 | throwOnFailure: Boolean = false, 187 | ): String? = if (s == null) null else decode(s, convertPlus, throwOnFailure) 188 | 189 | /** 190 | * Decodes '%'-escaped octets in the given string using the UTF-8 scheme. 191 | * Replaces invalid octets with the unicode replacement character 192 | * ("\\uFFFD"). 193 | * 194 | * @param s encoded string to decode 195 | * @param convertPlus if `convertPlus == true` all ‘+’ chars in the decoded output are converted to ‘ ‘ 196 | * (white space) 197 | * @param throwOnFailure if `throwOnFailure == true` an [IllegalArgumentException] is thrown for 198 | * invalid inputs. Else, U+FFd is emitted to the output in place of invalid input octets. 199 | * @return the given string with escaped octets decoded 200 | */ 201 | public fun decode( 202 | s: String, 203 | convertPlus: Boolean = false, 204 | throwOnFailure: Boolean = false, 205 | ): String { 206 | val builder = StringBuilder(s.length) 207 | 208 | // Holds the bytes corresponding to the escaped chars being read 209 | // (empty if the last char wasn't a escaped char). 210 | ByteBuffer(s.length).apply { 211 | var i = 0 212 | while (i < s.length) { 213 | when (val c = s[i++]) { 214 | '+' -> { 215 | flushDecodingByteAccumulator(builder, throwOnFailure) 216 | builder.append(if (convertPlus) ' ' else '+') 217 | } 218 | 219 | '%' -> { 220 | // Expect two characters representing a number in hex. 221 | var hexValue: Byte = 0 222 | for (@Suppress("UnusedPrivateProperty") j in 0..1) { 223 | val nextC = 224 | try { 225 | getNextCharacter(s, i, s.length, name = null) 226 | } catch (e: UriSyntaxException) { 227 | // Unexpected end of input. 228 | if (throwOnFailure) { 229 | throw IllegalArgumentException(e) 230 | } else { 231 | flushDecodingByteAccumulator(builder, throwOnFailure) 232 | builder.append(INVALID_INPUT_CHARACTER) 233 | return builder.toString() 234 | } 235 | } 236 | i++ 237 | val newDigit: Int = hexCharToValue(nextC) 238 | if (newDigit < 0) { 239 | if (throwOnFailure) { 240 | throw IllegalArgumentException( 241 | unexpectedCharacterException(s, name = null, nextC, i - 1), 242 | ) 243 | } else { 244 | flushDecodingByteAccumulator(builder, throwOnFailure) 245 | builder.append(INVALID_INPUT_CHARACTER) 246 | break 247 | } 248 | } 249 | hexValue = (hexValue * 0x10 + newDigit).toByte() 250 | } 251 | writeByte(hexValue) 252 | } 253 | 254 | else -> { 255 | flushDecodingByteAccumulator(builder, throwOnFailure) 256 | builder.append(c) 257 | } 258 | } 259 | } 260 | 261 | flushDecodingByteAccumulator(builder, throwOnFailure) 262 | } 263 | 264 | return builder.toString() 265 | } 266 | 267 | private class ByteBuffer( 268 | private val size: Int, 269 | ) { 270 | private val buffer by lazy { 271 | ByteArray(size) { 0 } 272 | } 273 | 274 | var writePosition = 0 275 | private set 276 | 277 | fun writeByte(byte: Byte) { 278 | buffer[writePosition++] = byte 279 | } 280 | 281 | fun decodeToStringAndReset() = 282 | try { 283 | buffer.decodeToString( 284 | startIndex = 0, 285 | endIndex = writePosition, 286 | throwOnInvalidSequence = false, 287 | ) 288 | } finally { 289 | writePosition = 0 290 | } 291 | } 292 | 293 | private fun ByteBuffer.flushDecodingByteAccumulator( 294 | builder: StringBuilder, 295 | throwOnFailure: Boolean, 296 | ) { 297 | if (writePosition == 0) return 298 | 299 | try { 300 | builder.append(decodeToStringAndReset()) 301 | } catch (e: Exception) { 302 | if (throwOnFailure) { 303 | throw IllegalArgumentException(e) 304 | } else { 305 | builder.append(INVALID_INPUT_CHARACTER) 306 | } 307 | } 308 | } 309 | 310 | private fun unexpectedCharacterException( 311 | uri: String, 312 | name: String?, 313 | unexpected: Char, 314 | index: Int, 315 | ): UriSyntaxException { 316 | val nameString = if (name == null) "" else " in [$name]" 317 | return UriSyntaxException( 318 | uri, 319 | "Unexpected character$nameString: $unexpected", 320 | index, 321 | ) 322 | } 323 | 324 | private fun getNextCharacter( 325 | uri: String, 326 | index: Int, 327 | end: Int, 328 | name: String?, 329 | ): Char { 330 | if (index >= end) { 331 | val nameString = if (name == null) "" else " in [$name]" 332 | throw UriSyntaxException(uri, "Unexpected end of string $nameString", index) 333 | } 334 | return uri[index] 335 | } 336 | 337 | /** 338 | * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f'). 339 | */ 340 | private fun hexCharToValue(c: Char): Int = 341 | when (c) { 342 | in digitAsciiRange -> c.code - '0'.code 343 | in lowercaseHexRange -> 10 + c.code - 'a'.code 344 | in uppercaseHexRange -> 10 + c.code - 'A'.code 345 | else -> -1 346 | } 347 | 348 | private val lowercaseAsciiAlphaRange = 'a'..'z' 349 | private val lowercaseHexRange = 'a'..'f' 350 | private val uppercaseAsciiAlphaRange = 'A'..'Z' 351 | private val uppercaseHexRange = 'A'..'F' 352 | private val digitAsciiRange = '0'..'9' 353 | private val defaultAllowedSet = setOf('_', '-', '!', '.', '~', '\'', '(', ')', '*') 354 | private val hexDigits = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') 355 | 356 | /** 357 | * Character to be output when there's an error decoding an input. 358 | */ 359 | private const val INVALID_INPUT_CHARACTER = '\ufffd' 360 | } 361 | --------------------------------------------------------------------------------