diff --git a/gradle.properties b/gradle.properties index 8154519..17113cd 100755 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,8 @@ kotlin.native.enableDependencyPropagation=false kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.mpp.enableCompatibilityMetadataVariant=true +kotlin.native.binary.memoryModel=experimental + android.useAndroidX=true mobile.multiplatform.iosTargetWarning=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b278877..9e40415 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlinVersion = "1.6.10" +kotlinVersion = "1.6.20" # android lifecycleViewModelVersion = "2.3.1" @@ -15,11 +15,11 @@ kotlinxSerializationVersion = "1.3.2" coroutinesVersion = "1.6.0-native-mt" # moko -mokoResourcesVersion = "0.18.0" +mokoResourcesVersion = "0.20.1" mokoMvvmVersion = "0.12.0" mokoErrorsVersion = "0.6.0" mokoTestVersion = "0.6.1" -mokoNetworkVersion = "0.18.0" +mokoNetworkVersion = "0.19.0" # tests espressoCoreVersion = "3.2.0" @@ -29,7 +29,7 @@ androidxTestVersion = "1.3.0" robolectricVersion = "4.6.1" # other -ktorClientVersion = "1.6.7" +ktorClientVersion = "2.0.1" kbignumVersion = "2.4.12" multidexVersion = "2.0.1" @@ -48,6 +48,7 @@ coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version ktorClientOkHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientVersion" } ktorClient = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientVersion" } ktorClientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientVersion" } +ktorClientWebSocket = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorClientVersion" } ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" } ktorClientIos = { module = "io.ktor:ktor-client-ios", version.ref = "ktorClientVersion" } diff --git a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache index d77db82..dea562e 100755 --- a/network-generator/src/main/resources/kotlin-ktor-client/api.mustache +++ b/network-generator/src/main/resources/kotlin-ktor-client/api.mustache @@ -4,10 +4,12 @@ package {{apiPackage}} {{#imports}}import {{import}} {{/imports}} +import io.ktor.client.call.body import io.ktor.client.HttpClient import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.request import io.ktor.client.request.forms.FormDataContent +import io.ktor.client.request.setBody import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.Parameters @@ -19,6 +21,8 @@ import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer import io.ktor.client.call.ReceivePipelineException import io.ktor.http.content.TextContent +import io.ktor.http.encodedPath +import io.ktor.http.path {{#operations}} {{>classes_modifiers}}interface {{classname}} { @@ -60,7 +64,7 @@ import io.ktor.http.content.TextContent takeFrom(_basePath) encodedPath = encodedPath.let { startingPath -> path("{{path}}") - return@let startingPath + encodedPath.substring(1) + return@let startingPath + encodedPath } {{#hasQueryParams}} @Suppress("UNNECESSARY_SAFE_CALL") @@ -79,7 +83,7 @@ import io.ktor.http.content.TextContent {{#bodyParams}} @Suppress("SENSELESS_COMPARISON") if({{paramName}} != null) { - builder.body = TextContent( + builder.setBody(TextContent( {{#dataType}} {{#isArray}} {{#uniqueItems}} @@ -106,7 +110,7 @@ import io.ktor.http.content.TextContent {{paramName}}), {{/dataType}} ContentType.Application.Json.withoutParameters() - ) + )) } {{/bodyParams}} @@ -116,7 +120,7 @@ import io.ktor.http.content.TextContent {{paramName}}?.let { append("{{baseName}}", it.toString()) } {{/formParams}} }) - builder.body = formData + builder.setBody(formData) {{/hasFormParams}} with(builder.headers) { append("Accept", "application/json") @@ -131,12 +135,12 @@ import io.ktor.http.content.TextContent {{#vendorExtensions.x-successResponse}} val serializer = {{>property_serializer}} //not primitive type - val result: String = _httpClient.request(builder) + val result: String = _httpClient.request(builder).body() return _json.decodeFromString(serializer, result) {{/vendorExtensions.x-successResponse}} {{/returnType}} {{^returnType}} - return _httpClient.request(builder) + return _httpClient.request(builder).body() {{/returnType}} } catch (pipeline: ReceivePipelineException) { throw pipeline.cause diff --git a/network/src/androidMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt b/network/src/androidMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt index 67838a0..7eed8ac 100644 --- a/network/src/androidMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt +++ b/network/src/androidMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt @@ -6,9 +6,9 @@ package dev.icerock.moko.network import android.content.res.Resources import androidx.core.os.ConfigurationCompat -import dev.icerock.moko.network.features.LanguageFeature +import dev.icerock.moko.network.plugins.LanguagePlugin -actual class LanguageProvider : LanguageFeature.LanguageCodeProvider { +actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider { override fun getLanguageCode(): String? { return ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0).language } diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpExt.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpExt.kt index 06ded1e..5e5686b 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpExt.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpExt.kt @@ -7,7 +7,9 @@ package dev.icerock.moko.network import dev.icerock.moko.network.exceptions.ResponseException import io.ktor.client.HttpClient import io.ktor.client.call.ReceivePipelineException +import io.ktor.client.call.body import io.ktor.client.request.request +import io.ktor.client.request.setBody import io.ktor.client.request.url import io.ktor.client.utils.EmptyContent import io.ktor.http.ContentType @@ -36,8 +38,8 @@ suspend inline fun HttpClient.createRequest( method = methodType url(path) if (contentType != null) contentType(contentType) - this.body = body - } + setBody(body) + }.body() } catch (e: ReceivePipelineException) { if (e.cause is ResponseException) { throw e.cause diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt index 9829d59..54eaa90 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt @@ -4,7 +4,7 @@ package dev.icerock.moko.network -import dev.icerock.moko.network.features.LanguageFeature +import dev.icerock.moko.network.plugins.LanguagePlugin @Suppress("EmptyDefaultConstructor") -expect class LanguageProvider() : LanguageFeature.LanguageCodeProvider +expect class LanguageProvider() : LanguagePlugin.LanguageCodeProvider diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/ExceptionFeature.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/features/ExceptionFeature.kt deleted file mode 100644 index 4d674a2..0000000 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/ExceptionFeature.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.network.features - -import dev.icerock.moko.network.exceptionfactory.ExceptionFactory -import io.ktor.client.HttpClient -import io.ktor.client.features.HttpClientFeature -import io.ktor.client.statement.HttpResponsePipeline -import io.ktor.http.isSuccess -import io.ktor.util.AttributeKey -import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.charsets.Charset -import io.ktor.utils.io.core.readText -import io.ktor.utils.io.readRemaining - -class ExceptionFeature(private val exceptionFactory: ExceptionFactory) { - - class Config { - var exceptionFactory: ExceptionFactory? = null - fun build() = ExceptionFeature( - exceptionFactory - ?: throw IllegalArgumentException("Exception factory should be contain") - ) - } - - companion object Feature : HttpClientFeature { - - override val key = AttributeKey("ExceptionFeature") - - override fun prepare(block: Config.() -> Unit) = Config().apply(block).build() - - override fun install(feature: ExceptionFeature, scope: HttpClient) { - scope.responsePipeline.intercept(HttpResponsePipeline.Receive) { (_, body) -> - if (body !is ByteReadChannel) return@intercept - - val response = context.response - if (!response.status.isSuccess()) { - val packet = body.readRemaining() - val responseString = packet.readText(charset = Charset.forName("UTF-8")) - throw feature.exceptionFactory.createException( - request = context.request, - response = context.response, - responseBody = responseString - ) - } - proceedWith(subject) - } - } - } -} diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/DynamicUserAgent.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/DynamicUserAgent.kt similarity index 88% rename from network/src/commonMain/kotlin/dev/icerock/moko/network/features/DynamicUserAgent.kt rename to network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/DynamicUserAgent.kt index cdce6e4..b693197 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/DynamicUserAgent.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/DynamicUserAgent.kt @@ -5,7 +5,7 @@ package dev.icerock.moko.network.features import io.ktor.client.HttpClient -import io.ktor.client.features.HttpClientFeature +import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.request.HttpRequestPipeline import io.ktor.client.request.header import io.ktor.http.HttpHeaders @@ -16,7 +16,7 @@ class DynamicUserAgent( ) { class Config(var agentProvider: () -> String? = { null }) - companion object Feature : HttpClientFeature { + companion object Feature : HttpClientPlugin { override val key: AttributeKey = AttributeKey("DynamicUserAgent") override fun prepare(block: Config.() -> Unit): DynamicUserAgent = diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/ExceptionPlugin.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/ExceptionPlugin.kt new file mode 100644 index 0000000..99fd63a --- /dev/null +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/ExceptionPlugin.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.network.plugins + +import dev.icerock.moko.network.exceptionfactory.ExceptionFactory +import io.ktor.client.HttpClient +import io.ktor.client.plugins.HttpClientPlugin +import io.ktor.client.plugins.HttpSend +import io.ktor.client.plugins.plugin +import io.ktor.client.statement.bodyAsChannel +import io.ktor.http.isSuccess +import io.ktor.util.AttributeKey +import io.ktor.utils.io.charsets.Charset +import io.ktor.utils.io.core.readText + +class ExceptionPlugin(private val exceptionFactory: ExceptionFactory) { + + class Config { + var exceptionFactory: ExceptionFactory? = null + fun build() = ExceptionPlugin( + exceptionFactory + ?: throw IllegalArgumentException("Exception factory should be contain") + ) + } + + companion object Plugin : HttpClientPlugin { + + override val key = AttributeKey("ExceptionPlugin") + + override fun prepare(block: Config.() -> Unit) = Config().apply(block).build() + + override fun install(plugin: ExceptionPlugin, scope: HttpClient) { + scope.plugin(HttpSend).intercept { request -> + val call = execute(request) + if (!call.response.status.isSuccess()) { + val packet = call.response.bodyAsChannel().readRemaining() + val responseString = packet.readText(charset = Charset.forName("UTF-8")) + throw plugin.exceptionFactory.createException( + request = call.request, + response = call.response, + responseBody = responseString + ) + } + call + } + } + } +} diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/LanguageFeature.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/LanguagePlugin.kt similarity index 61% rename from network/src/commonMain/kotlin/dev/icerock/moko/network/features/LanguageFeature.kt rename to network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/LanguagePlugin.kt index cbeb573..d2cc374 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/LanguageFeature.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/LanguagePlugin.kt @@ -2,37 +2,37 @@ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.network.features +package dev.icerock.moko.network.plugins import io.ktor.client.HttpClient -import io.ktor.client.features.HttpClientFeature +import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.request.HttpRequestPipeline import io.ktor.client.request.header import io.ktor.util.AttributeKey -class LanguageFeature private constructor( +class LanguagePlugin private constructor( private val languageHeaderName: String, - private val languageProvider: LanguageFeature.LanguageCodeProvider + private val languageProvider: LanguagePlugin.LanguageCodeProvider ) { class Config { var languageHeaderName: String? = null var languageCodeProvider: LanguageCodeProvider? = null - fun build() = LanguageFeature( + fun build() = LanguagePlugin( languageHeaderName ?: throw IllegalArgumentException("HeaderName should be contain"), languageCodeProvider ?: throw IllegalArgumentException("LanguageCodeProvider should be contain") ) } - companion object Feature : HttpClientFeature { - override val key = AttributeKey("LanguageFeature") + companion object Plugin : HttpClientPlugin { + override val key = AttributeKey("LanguagePlugin") override fun prepare(block: Config.() -> Unit) = Config().apply(block).build() - override fun install(feature: LanguageFeature, scope: HttpClient) { + override fun install(plugin: LanguagePlugin, scope: HttpClient) { scope.requestPipeline.intercept(HttpRequestPipeline.State) { - feature.languageProvider.getLanguageCode()?.apply { - context.header(feature.languageHeaderName, this) + plugin.languageProvider.getLanguageCode()?.apply { + context.header(plugin.languageHeaderName, this) } } } diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/RefreshTokenFeature.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt similarity index 58% rename from network/src/commonMain/kotlin/dev/icerock/moko/network/features/RefreshTokenFeature.kt rename to network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt index d36a961..aadc94f 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/RefreshTokenFeature.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt @@ -2,21 +2,22 @@ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.network.features +package dev.icerock.moko.network.plugins import io.ktor.client.HttpClient -import io.ktor.client.features.HttpClientFeature +import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.request.HttpRequest import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.request import io.ktor.client.request.takeFrom import io.ktor.client.statement.HttpReceivePipeline import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.request import io.ktor.http.HttpStatusCode import io.ktor.util.AttributeKey import kotlinx.coroutines.sync.Mutex -class RefreshTokenFeature( +class RefreshTokenPlugin( private val updateTokenHandler: suspend () -> Boolean, private val isCredentialsActual: (HttpRequest) -> Boolean ) { @@ -25,50 +26,52 @@ class RefreshTokenFeature( var updateTokenHandler: (suspend () -> Boolean)? = null var isCredentialsActual: ((HttpRequest) -> Boolean)? = null - fun build() = RefreshTokenFeature( - updateTokenHandler ?: throw IllegalArgumentException("updateTokenHandler should be passed"), - isCredentialsActual ?: throw IllegalArgumentException("isCredentialsActual should be passed") + fun build() = RefreshTokenPlugin( + updateTokenHandler + ?: throw IllegalArgumentException("updateTokenHandler should be passed"), + isCredentialsActual + ?: throw IllegalArgumentException("isCredentialsActual should be passed") ) } - companion object Feature : HttpClientFeature { + companion object Plugin : HttpClientPlugin { - private val refreshTokenHttpFeatureMutex = Mutex() + private val refreshTokenHttpPluginMutex = Mutex() - override val key = AttributeKey("RefreshTokenFeature") + override val key = AttributeKey("RefreshTokenPlugin") override fun prepare(block: Config.() -> Unit) = Config().apply(block).build() - override fun install(feature: RefreshTokenFeature, scope: HttpClient) { - scope.receivePipeline.intercept(HttpReceivePipeline.After) { subject -> - if (context.response.status != HttpStatusCode.Unauthorized) { + override fun install(plugin: RefreshTokenPlugin, scope: HttpClient) { + scope.receivePipeline.intercept(HttpReceivePipeline.After) { + if (subject.status != HttpStatusCode.Unauthorized) { proceedWith(subject) return@intercept } - refreshTokenHttpFeatureMutex.lock() + refreshTokenHttpPluginMutex.lock() // If token of the request isn't actual, then token has already been updated and // let's just to try repeat request - if (!feature.isCredentialsActual(context.request)) { - refreshTokenHttpFeatureMutex.unlock() - val requestBuilder = HttpRequestBuilder().takeFrom(context.request) - val result: HttpResponse = context.client!!.request(requestBuilder) + if (!plugin.isCredentialsActual(subject.request)) { + refreshTokenHttpPluginMutex.unlock() + val requestBuilder = HttpRequestBuilder().takeFrom(subject.request) + val result: HttpResponse = scope.request(requestBuilder) proceedWith(result) return@intercept } // Else if token of the request is actual (same as in the storage), then need to send // refresh request. - if (feature.updateTokenHandler.invoke()) { + if (plugin.updateTokenHandler.invoke()) { // If the request refresh was successful, then let's just to try repeat request - refreshTokenHttpFeatureMutex.unlock() - val requestBuilder = HttpRequestBuilder().takeFrom(context.request) - val result: HttpResponse = context.client!!.request(requestBuilder) + refreshTokenHttpPluginMutex.unlock() + val requestBuilder = HttpRequestBuilder().takeFrom(subject.request) + val result: HttpResponse = scope.request(requestBuilder) proceedWith(result) } else { // If the request refresh was unsuccessful - refreshTokenHttpFeatureMutex.unlock() + refreshTokenHttpPluginMutex.unlock() proceedWith(subject) } } diff --git a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/TokenFeature.kt b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt similarity index 62% rename from network/src/commonMain/kotlin/dev/icerock/moko/network/features/TokenFeature.kt rename to network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt index 45fd183..5a5b2b3 100644 --- a/network/src/commonMain/kotlin/dev/icerock/moko/network/features/TokenFeature.kt +++ b/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt @@ -2,15 +2,15 @@ * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.network.features +package dev.icerock.moko.network.plugins import io.ktor.client.HttpClient -import io.ktor.client.features.HttpClientFeature +import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.request.HttpRequestPipeline import io.ktor.client.request.header import io.ktor.util.AttributeKey -class TokenFeature private constructor( +class TokenPlugin private constructor( private val tokenHeaderName: String, private val tokenProvider: TokenProvider ) { @@ -18,22 +18,22 @@ class TokenFeature private constructor( class Config { var tokenHeaderName: String? = null var tokenProvider: TokenProvider? = null - fun build() = TokenFeature( + fun build() = TokenPlugin( tokenHeaderName ?: throw IllegalArgumentException("HeaderName should be contain"), tokenProvider ?: throw IllegalArgumentException("TokenProvider should be contain") ) } - companion object Feature : HttpClientFeature { - override val key = AttributeKey("TokenFeature") + companion object Plugin : HttpClientPlugin { + override val key = AttributeKey("TokenPlugin") override fun prepare(block: Config.() -> Unit) = Config().apply(block).build() - override fun install(feature: TokenFeature, scope: HttpClient) { + override fun install(plugin: TokenPlugin, scope: HttpClient) { scope.requestPipeline.intercept(HttpRequestPipeline.State) { - feature.tokenProvider.getToken()?.apply { - context.headers.remove(feature.tokenHeaderName) - context.header(feature.tokenHeaderName, this) + plugin.tokenProvider.getToken()?.apply { + context.headers.remove(plugin.tokenHeaderName) + context.header(plugin.tokenHeaderName, this) } } } diff --git a/network/src/commonTest/kotlin/ExceptionFeatureTest.kt b/network/src/commonTest/kotlin/ExceptionFeatureTest.kt index cac2f8d..a7fdc6a 100644 --- a/network/src/commonTest/kotlin/ExceptionFeatureTest.kt +++ b/network/src/commonTest/kotlin/ExceptionFeatureTest.kt @@ -8,15 +8,13 @@ import dev.icerock.moko.network.exceptionfactory.parser.ValidationExceptionParse import dev.icerock.moko.network.exceptions.ErrorException import dev.icerock.moko.network.exceptions.ResponseException import dev.icerock.moko.network.exceptions.ValidationException -import dev.icerock.moko.network.features.ExceptionFeature +import dev.icerock.moko.network.plugins.ExceptionPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandler -import io.ktor.client.engine.mock.respondBadRequest import io.ktor.client.engine.mock.respondError import io.ktor.client.engine.mock.respondOk import io.ktor.client.request.get -import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json @@ -32,7 +30,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = false, actual = result.isFailure) @@ -55,7 +53,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -82,7 +80,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -107,7 +105,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -131,7 +129,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -166,7 +164,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -202,7 +200,7 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } assertEquals(expected = true, actual = result.isFailure) @@ -229,9 +227,8 @@ class ExceptionFeatureTest { } val result = runBlocking { - kotlin.runCatching { client.get("localhost") } + kotlin.runCatching { client.get("localhost") } } - assertEquals(expected = true, actual = result.isFailure) val exc = result.exceptionOrNull() assertTrue(actual = exc is ResponseException) @@ -247,7 +244,7 @@ class ExceptionFeatureTest { addHandler(handler) } - install(ExceptionFeature) { + install(ExceptionPlugin) { exceptionFactory = HttpExceptionFactory( defaultParser = ErrorExceptionParser(json), customParsers = mapOf( diff --git a/network/src/commonTest/kotlin/LanguageFeatureTest.kt b/network/src/commonTest/kotlin/LanguageFeatureTest.kt index 3e1aad1..9a9d2c7 100644 --- a/network/src/commonTest/kotlin/LanguageFeatureTest.kt +++ b/network/src/commonTest/kotlin/LanguageFeatureTest.kt @@ -2,8 +2,7 @@ * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -import dev.icerock.moko.network.features.LanguageFeature -import dev.icerock.moko.network.features.TokenFeature +import dev.icerock.moko.network.plugins.LanguagePlugin import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandler @@ -20,7 +19,7 @@ class LanguageFeatureTest { @Test fun `language added when exist`() { val client = createMockClient( - provider = object : LanguageFeature.LanguageCodeProvider { + provider = object : LanguagePlugin.LanguageCodeProvider { override fun getLanguageCode(): String = "ru" }, handler = { request -> @@ -30,7 +29,7 @@ class LanguageFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) @@ -39,7 +38,7 @@ class LanguageFeatureTest { @Test fun `language not added when not exist`() { val client = createMockClient( - provider = object : LanguageFeature.LanguageCodeProvider { + provider = object : LanguagePlugin.LanguageCodeProvider { override fun getLanguageCode(): String? = null }, handler = { request -> @@ -49,14 +48,14 @@ class LanguageFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) } private fun createMockClient( - provider: LanguageFeature.LanguageCodeProvider, + provider: LanguagePlugin.LanguageCodeProvider, handler: MockRequestHandler ): HttpClient { return HttpClient(MockEngine) { @@ -64,7 +63,7 @@ class LanguageFeatureTest { addHandler(handler) } - install(LanguageFeature) { + install(LanguagePlugin) { this.languageHeaderName = LANGUAGE_HEADER_NAME this.languageCodeProvider = provider } diff --git a/network/src/commonTest/kotlin/RefreshTokenFeatureTest.kt b/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt similarity index 79% rename from network/src/commonTest/kotlin/RefreshTokenFeatureTest.kt rename to network/src/commonTest/kotlin/RefreshTokenPluginTest.kt index c935660..957db2a 100644 --- a/network/src/commonTest/kotlin/RefreshTokenFeatureTest.kt +++ b/network/src/commonTest/kotlin/RefreshTokenPluginTest.kt @@ -2,32 +2,27 @@ * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -import dev.icerock.moko.network.features.RefreshTokenFeature -import dev.icerock.moko.network.features.TokenFeature +import dev.icerock.moko.network.plugins.RefreshTokenPlugin +import dev.icerock.moko.network.plugins.TokenPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandler -import io.ktor.client.engine.mock.respondBadRequest import io.ktor.client.engine.mock.respondError import io.ktor.client.engine.mock.respondOk import io.ktor.client.request.get -import io.ktor.client.request.header -import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.request import io.ktor.http.HttpStatusCode import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.Json -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -class RefreshTokenFeatureTest { +class RefreshTokenPluginTest { @Test fun `refresh token not called when request credentials not actual`() { val isFirstRequestHolder = MutableStateFlow(true) val client = createMockClient( - featureConfig = { + pluginConfig = { this.updateTokenHandler = { throw IllegalStateException("update token should not be called at all") } @@ -42,7 +37,7 @@ class RefreshTokenFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) @@ -54,12 +49,12 @@ class RefreshTokenFeatureTest { val validToken = "124" val tokenHolder = MutableStateFlow(invalidToken) val client = createMockClient( - tokenProvider = object : TokenFeature.TokenProvider { + tokenProvider = object : TokenPlugin.TokenProvider { override fun getToken(): String? { return tokenHolder.value } }, - featureConfig = { + pluginConfig = { this.updateTokenHandler = { tokenHolder.value = validToken true @@ -76,7 +71,7 @@ class RefreshTokenFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) @@ -84,8 +79,8 @@ class RefreshTokenFeatureTest { } private fun createMockClient( - tokenProvider: TokenFeature.TokenProvider? = null, - featureConfig: RefreshTokenFeature.Config.() -> Unit, + tokenProvider: TokenPlugin.TokenProvider? = null, + pluginConfig: RefreshTokenPlugin.Config.() -> Unit, handler: MockRequestHandler ): HttpClient { return HttpClient(MockEngine) { @@ -94,12 +89,12 @@ class RefreshTokenFeatureTest { } if (tokenProvider != null) { - install(TokenFeature) { + install(TokenPlugin) { this.tokenHeaderName = AUTH_HEADER_NAME this.tokenProvider = tokenProvider } } - install(RefreshTokenFeature, featureConfig) + install(RefreshTokenPlugin, pluginConfig) } } diff --git a/network/src/commonTest/kotlin/TokenFeatureTest.kt b/network/src/commonTest/kotlin/TokenFeatureTest.kt index 378612a..49a1f94 100644 --- a/network/src/commonTest/kotlin/TokenFeatureTest.kt +++ b/network/src/commonTest/kotlin/TokenFeatureTest.kt @@ -2,18 +2,15 @@ * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -import dev.icerock.moko.network.features.TokenFeature +import dev.icerock.moko.network.plugins.TokenPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandler import io.ktor.client.engine.mock.respondBadRequest import io.ktor.client.engine.mock.respondOk import io.ktor.client.request.get -import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.Json -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -21,7 +18,7 @@ class TokenFeatureTest { @Test fun `token added when exist`() { val client = createMockClient( - tokenProvider = object : TokenFeature.TokenProvider { + tokenProvider = object : TokenPlugin.TokenProvider { override fun getToken(): String { return "mytoken" } @@ -33,7 +30,7 @@ class TokenFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) @@ -42,7 +39,7 @@ class TokenFeatureTest { @Test fun `token not added when not exist`() { val client = createMockClient( - tokenProvider = object : TokenFeature.TokenProvider { + tokenProvider = object : TokenPlugin.TokenProvider { override fun getToken(): String? { return null } @@ -54,14 +51,14 @@ class TokenFeatureTest { ) val result = runBlocking { - client.get("localhost") + client.get("localhost") } assertEquals(expected = HttpStatusCode.OK, actual = result.status) } private fun createMockClient( - tokenProvider: TokenFeature.TokenProvider, + tokenProvider: TokenPlugin.TokenProvider, handler: MockRequestHandler ): HttpClient { return HttpClient(MockEngine) { @@ -69,7 +66,7 @@ class TokenFeatureTest { addHandler(handler) } - install(TokenFeature) { + install(TokenPlugin) { this.tokenHeaderName = AUTH_HEADER_NAME this.tokenProvider = tokenProvider } diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt deleted file mode 100644 index e5a5d35..0000000 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.network - -import io.ktor.client.features.websocket.WebSocketException -import io.ktor.http.cio.websocket.CloseReason -import io.ktor.http.cio.websocket.DefaultWebSocketSession -import io.ktor.http.cio.websocket.ExperimentalWebSocketExtensionApi -import io.ktor.http.cio.websocket.Frame -import io.ktor.http.cio.websocket.WebSocketExtension -import io.ktor.http.cio.websocket.readText -import io.ktor.util.InternalAPI -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.launch -import platform.Foundation.NSData -import platform.Foundation.NSError -import platform.Foundation.NSOperationQueue -import platform.Foundation.NSPOSIXErrorDomain -import platform.Foundation.NSURL -import platform.Foundation.NSURLSession -import platform.Foundation.NSURLSessionConfiguration -import platform.Foundation.NSURLSessionWebSocketCloseCode -import platform.Foundation.NSURLSessionWebSocketDelegateProtocol -import platform.Foundation.NSURLSessionWebSocketMessage -import platform.Foundation.NSURLSessionWebSocketTask -import platform.darwin.NSObject -import kotlin.coroutines.CoroutineContext - -internal class IosWebSocket( - socketEndpoint: NSURL, - override val coroutineContext: CoroutineContext -) : DefaultWebSocketSession { - internal val originResponse: CompletableDeferred = CompletableDeferred() - - private val webSocket: NSURLSessionWebSocketTask - - private val _incoming = Channel() - private val _outgoing = Channel() - private val _closeReason = CompletableDeferred() - - override val incoming: ReceiveChannel = _incoming - override val outgoing: SendChannel = _outgoing - override val closeReason: Deferred = _closeReason - - @ExperimentalWebSocketExtensionApi - override val extensions: List> - get() = emptyList() - - override var maxFrameSize: Long - get() = throw WebSocketException("websocket doesn't support max frame size.") - set(_) = throw WebSocketException("websocket doesn't support max frame size.") - - override suspend fun flush() = Unit - - @OptIn(ExperimentalWebSocketExtensionApi::class) - @InternalAPI - override fun start(negotiatedExtensions: List>) { - require(negotiatedExtensions.isEmpty()) { "Extensions are not supported." } - } - - init { - val urlSession = NSURLSession.sessionWithConfiguration( - configuration = NSURLSessionConfiguration.defaultSessionConfiguration(), - delegate = object : NSObject(), NSURLSessionWebSocketDelegateProtocol { - override fun URLSession( - session: NSURLSession, - webSocketTask: NSURLSessionWebSocketTask, - didOpenWithProtocol: String? - ) { - originResponse.complete(didOpenWithProtocol) - } - - override fun URLSession( - session: NSURLSession, - webSocketTask: NSURLSessionWebSocketTask, - didCloseWithCode: NSURLSessionWebSocketCloseCode, - reason: NSData? - ) { - val closeReason = CloseReason( - code = CloseReason.Codes.PROTOCOL_ERROR, - message = "$didCloseWithCode : ${reason.toString()}" - ) - _closeReason.complete(closeReason) - } - }, - delegateQueue = NSOperationQueue.currentQueue() - ) - webSocket = urlSession.webSocketTaskWithURL(socketEndpoint) - - CoroutineScope(coroutineContext).launch { - _outgoing.consumeEach { frame -> - if (frame is Frame.Text) { - val message = NSURLSessionWebSocketMessage(frame.readText()) - webSocket.sendMessage(message) { nsError -> - if (nsError == null) return@sendMessage - - nsError.closeSocketOrThrow { - throw SendMessageException(nsError.description ?: nsError.toString()) - } - } - } - } - } - - listenMessages() - } - - fun start() { - webSocket.resume() - } - - private fun listenMessages() { - webSocket.receiveMessageWithCompletionHandler { message, nsError -> - when { - nsError != null -> { - nsError.closeSocketOrThrow { - throw ReceiveMessageException(nsError.description ?: nsError.toString()) - } - } - message != null -> { - message.string?.let { _incoming.trySend(Frame.Text(it)) } - } - } - listenMessages() - } - } - - private fun NSError.closeSocketOrThrow(throwBlock: () -> Unit) { - if (domain !in listOf("kNWErrorDomainPOSIX", NSPOSIXErrorDomain)) return throwBlock() - if (code != 57L) return throwBlock() - - val closeReason = CloseReason( - code = CloseReason.Codes.NORMAL, - message = description ?: toString() - ) - _closeReason.complete(closeReason) - webSocket.cancel() - } - - override fun terminate() { - coroutineContext.cancel() - } -} diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt index 8ccf49b..22e846d 100644 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt +++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt @@ -4,13 +4,13 @@ package dev.icerock.moko.network -import dev.icerock.moko.network.features.LanguageFeature +import dev.icerock.moko.network.plugins.LanguagePlugin import platform.Foundation.NSLocale import platform.Foundation.currentLocale import platform.Foundation.languageCode -actual class LanguageProvider : LanguageFeature.LanguageCodeProvider { - override fun getLanguageCode(): String? { +actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider { + override fun getLanguageCode(): String { return NSLocale.currentLocale.languageCode } } diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt index 9424124..d051375 100644 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt +++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt @@ -4,11 +4,11 @@ package dev.icerock.moko.network -import io.ktor.client.engine.ios.IosHttpRequestException +import io.ktor.client.engine.darwin.DarwinHttpRequestException actual fun Throwable.isNetworkConnectionError(): Boolean { return when (this) { - is IosHttpRequestException -> isSSLException().not() + is DarwinHttpRequestException -> isSSLException().not() else -> false } } diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/ReceiveMessageException.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/ReceiveMessageException.kt deleted file mode 100644 index c49b261..0000000 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/ReceiveMessageException.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.network - -import io.ktor.utils.io.errors.IOException - -class ReceiveMessageException(message: String) : IOException(message) diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/SendMessageException.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/SendMessageException.kt deleted file mode 100644 index 993a4db..0000000 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/SendMessageException.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.network - -import io.ktor.utils.io.errors.IOException - -class SendMessageException(message: String) : IOException(message) diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/WSIosHttpClientEngine.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/WSIosHttpClientEngine.kt deleted file mode 100644 index ac5e124..0000000 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/WSIosHttpClientEngine.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.network - -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.HttpClientEngineCapability -import io.ktor.client.engine.HttpClientEngineConfig -import io.ktor.client.engine.callContext -import io.ktor.client.features.websocket.WebSocketCapability -import io.ktor.client.request.HttpRequestData -import io.ktor.client.request.HttpResponseData -import io.ktor.client.request.isUpgradeRequest -import io.ktor.http.Headers -import io.ktor.http.HttpProtocolVersion -import io.ktor.http.HttpStatusCode -import io.ktor.util.InternalAPI -import io.ktor.util.date.GMTDate -import kotlinx.coroutines.CoroutineDispatcher -import platform.Foundation.NSURL -import kotlin.coroutines.CoroutineContext - -class WSIosHttpClientEngine( - private val wrappedEngine: HttpClientEngine -) : HttpClientEngine { - - override val supportedCapabilities: Set> - get() = wrappedEngine.supportedCapabilities + setOf(WebSocketCapability) - - override val config: HttpClientEngineConfig - get() = wrappedEngine.config - - override val dispatcher: CoroutineDispatcher - get() = wrappedEngine.dispatcher - - override val coroutineContext: CoroutineContext - get() = wrappedEngine.coroutineContext - - @InternalAPI - override suspend fun execute(data: HttpRequestData): HttpResponseData { - - val callContext = callContext() - return if (data.isUpgradeRequest()) { - executeWebSocketRequest(data, callContext) - } else { - wrappedEngine.execute(data) - } - } - - private suspend fun executeWebSocketRequest( - data: HttpRequestData, - callContext: CoroutineContext - ): HttpResponseData { - val requestTime = GMTDate() - val url: String = data.url.toString() - val socketEndpoint = NSURL.URLWithString(url)!! - - val session = IosWebSocket(socketEndpoint, callContext).apply { start() } - - val originResponse = session.originResponse.await() - - return HttpResponseData( - statusCode = HttpStatusCode.OK, - requestTime = requestTime, - headers = Headers.Empty, - version = HttpProtocolVersion.HTTP_1_0, // read from originResponse - body = session, - callContext = callContext - ) - } - - override fun close() { - wrappedEngine.close() - } -} diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt index a9eec98..5e2670b 100644 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt +++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt @@ -5,14 +5,14 @@ package dev.icerock.moko.network import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.ios.Ios +import io.ktor.client.engine.darwin.Darwin actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): HttpClientEngine { val config = HttpClientEngineConfig().also(block) - return Ios.create { + return Darwin.create { this.configureSession { config.iosTimeoutIntervalForRequest?.let { setTimeoutIntervalForRequest(it) } config.iosTimeoutIntervalForResource?.let { setTimeoutIntervalForResource(it) } } - }.let { WSIosHttpClientEngine(it) } + } } diff --git a/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt b/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt index 6dea821..087c057 100644 --- a/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt +++ b/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt @@ -4,7 +4,7 @@ package dev.icerock.moko.network -import io.ktor.client.engine.ios.IosHttpRequestException +import io.ktor.client.engine.darwin.DarwinHttpRequestException import platform.Foundation.NSURLErrorCannotLoadFromNetwork import platform.Foundation.NSURLErrorClientCertificateRequired import platform.Foundation.NSURLErrorSecureConnectionFailed @@ -24,13 +24,13 @@ private val sslKeys = mapOf( ) actual fun Throwable.isSSLException(): Boolean { - val iosHttpException = this as? IosHttpRequestException ?: return false + val iosHttpException = this as? DarwinHttpRequestException ?: return false return sslKeys.keys.contains( iosHttpException.origin.code ) } actual fun Throwable.getSSLExceptionType(): SSLExceptionType? { - val iosHttpException = this as? IosHttpRequestException ?: return null + val iosHttpException = this as? DarwinHttpRequestException ?: return null return sslKeys[iosHttpException.origin.code] } diff --git a/network/src/jvmMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt b/network/src/jvmMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt index e610310..86d2d8b 100644 --- a/network/src/jvmMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt +++ b/network/src/jvmMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt @@ -1,9 +1,9 @@ package dev.icerock.moko.network -import dev.icerock.moko.network.features.LanguageFeature +import dev.icerock.moko.network.plugins.LanguagePlugin import java.util.Locale -actual class LanguageProvider : LanguageFeature.LanguageCodeProvider { +actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider { override fun getLanguageCode(): String? { return Locale.getDefault().displayLanguage } diff --git a/sample/mpp-library/build.gradle.kts b/sample/mpp-library/build.gradle.kts index 89cf8e7..0c7de12 100644 --- a/sample/mpp-library/build.gradle.kts +++ b/sample/mpp-library/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { commonMainImplementation(libs.ktorClient) commonMainImplementation(libs.ktorClientLogging) commonMainImplementation(libs.kotlinSerialization) + commonMainImplementation(libs.ktorClientWebSocket) commonMainImplementation(libs.kbignum) commonMainApi(libs.mokoMvvmCore) @@ -98,3 +99,32 @@ mokoNetwork { inputSpec = file("src/requestHeaders.yaml") } } + +val copyIosX64TestResources = tasks.register("copyIosX64TestResources") { + from("src/commonTest/resources") + into("build/bin/iosX64/debugTest/resources") +} + +tasks.matching { it.name == "iosX64Test" }.configureEach { + dependsOn(copyIosX64TestResources) +} + +val copyIosArm64TestResources = tasks.register("copyIosArm64TestResources") { + from("src/commonTest/resources") + into("build/bin/iosSimulatorArm64/debugTest/resources") +} + +tasks.matching { it.name == "iosSimulatorArm64Test" }.configureEach { + dependsOn(copyIosArm64TestResources) +} + +tasks.withType() + .matching { it.name.contains("UnitTest") } + .configureEach { + doLast { + val testResourcesDir = File(projectDir, "src/commonTest/resources") + if (testResourcesDir.exists().not()) return@doLast + testResourcesDir.copyRecursively(destinationDir, overwrite = true) + } + } + diff --git a/sample/mpp-library/src/androidTest/kotlin/readResourceText.kt b/sample/mpp-library/src/androidTest/kotlin/readResourceText.kt new file mode 100644 index 0000000..a2b2c10 --- /dev/null +++ b/sample/mpp-library/src/androidTest/kotlin/readResourceText.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.utils + +import java.io.InputStream +import kotlin.test.assertNotNull + +actual fun Any.readResourceText(path: String): String { + val classLoader: ClassLoader? = this.javaClass.classLoader + assertNotNull(classLoader, "can't get classLoader of $this") + val resource: InputStream? = classLoader.getResourceAsStream(path) + assertNotNull(resource, "can't find resource with path [$path]") + return resource + .bufferedReader() + .readText() +} diff --git a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt index 4700f0e..cb0b7e2 100644 --- a/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt +++ b/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/TestViewModel.kt @@ -4,7 +4,6 @@ package com.icerockdev.library -import dev.icerock.moko.errors.MR import dev.icerock.moko.errors.handler.ExceptionHandler import dev.icerock.moko.errors.mappers.ExceptionMappersStorage import dev.icerock.moko.errors.presenters.AlertErrorPresenter @@ -14,19 +13,17 @@ import dev.icerock.moko.mvvm.livedata.readOnly import dev.icerock.moko.mvvm.viewmodel.ViewModel import dev.icerock.moko.network.LanguageProvider import dev.icerock.moko.network.createHttpClientEngine -import dev.icerock.moko.network.features.LanguageFeature -import dev.icerock.moko.network.features.TokenFeature import dev.icerock.moko.network.generated.apis.PetApi -import dev.icerock.moko.resources.desc.desc +import dev.icerock.moko.network.plugins.LanguagePlugin +import dev.icerock.moko.network.plugins.TokenPlugin import io.ktor.client.HttpClient -import io.ktor.client.features.logging.LogLevel -import io.ktor.client.features.logging.Logger -import io.ktor.client.features.logging.Logging -import io.ktor.client.features.websocket.WebSockets -import io.ktor.client.features.websocket.webSocket -import io.ktor.http.cio.websocket.Frame -import io.ktor.http.cio.websocket.readText -import io.ktor.http.cio.websocket.send +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocket +import io.ktor.websocket.Frame +import io.ktor.websocket.readText import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -35,17 +32,13 @@ import news.apis.NewsApi class TestViewModel : ViewModel() { val exceptionHandler = ExceptionHandler( - errorPresenter = AlertErrorPresenter( - // temporary fix https://youtrack.jetbrains.com/issue/KT-41823 - alertTitle = MR.strings.moko_errors_presenters_alertDialogTitle.desc(), - positiveButtonText = MR.strings.moko_errors_presenters_alertPositiveButton.desc() - ), + errorPresenter = AlertErrorPresenter(), exceptionMapper = ExceptionMappersStorage.throwableMapper(), onCatch = { it.printStackTrace() } ) private val httpClient = HttpClient { - install(LanguageFeature) { + install(LanguagePlugin) { languageHeaderName = "X-Language" languageCodeProvider = LanguageProvider() } @@ -58,10 +51,10 @@ class TestViewModel : ViewModel() { } } - install(TokenFeature) { + install(TokenPlugin) { tokenHeaderName = "Authorization" - tokenProvider = object : TokenFeature.TokenProvider { - override fun getToken(): String? = "ed155d0a445e4b4fbd878fe1f3bc1b7f" + tokenProvider = object : TokenPlugin.TokenProvider { + override fun getToken(): String = "ed155d0a445e4b4fbd878fe1f3bc1b7f" } } } @@ -132,7 +125,7 @@ class TestViewModel : ViewModel() { } } } - send("Hello world!") + send(Frame.Text("Hello world!")) _websocketInfo.value += "send first message\n" incomingJob.join() diff --git a/sample/mpp-library/src/commonTest/kotlin/PetApiTest.kt b/sample/mpp-library/src/commonTest/kotlin/PetApiTest.kt index e5267dd..3051118 100644 --- a/sample/mpp-library/src/commonTest/kotlin/PetApiTest.kt +++ b/sample/mpp-library/src/commonTest/kotlin/PetApiTest.kt @@ -8,6 +8,7 @@ import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respondOk import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json +import tests.utils.readResourceText import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -24,7 +25,7 @@ class PetApiTest { httpClient = HttpClient(MockEngine) { engine { addHandler { request -> - respondOk(content = petstoreSearchResponse) + respondOk(content = readResourceText("PetstoreSearchResponse.json")) } } } diff --git a/sample/mpp-library/src/commonTest/kotlin/createHttpClient.kt b/sample/mpp-library/src/commonTest/kotlin/createHttpClient.kt index aafa8de..7b776ae 100644 --- a/sample/mpp-library/src/commonTest/kotlin/createHttpClient.kt +++ b/sample/mpp-library/src/commonTest/kotlin/createHttpClient.kt @@ -5,7 +5,7 @@ import dev.icerock.moko.network.exceptionfactory.HttpExceptionFactory import dev.icerock.moko.network.exceptionfactory.parser.ErrorExceptionParser import dev.icerock.moko.network.exceptionfactory.parser.ValidationExceptionParser -import dev.icerock.moko.network.features.ExceptionFeature +import dev.icerock.moko.network.plugins.ExceptionPlugin import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.MockRequestHandler @@ -23,7 +23,7 @@ fun createMockClient( addHandler(handler) } - install(ExceptionFeature) { + install(ExceptionPlugin) { exceptionFactory = HttpExceptionFactory( defaultParser = ErrorExceptionParser(json), customParsers = mapOf( diff --git a/sample/mpp-library/src/commonTest/kotlin/tests/utils/readResourceText.kt b/sample/mpp-library/src/commonTest/kotlin/tests/utils/readResourceText.kt new file mode 100644 index 0000000..9228e9e --- /dev/null +++ b/sample/mpp-library/src/commonTest/kotlin/tests/utils/readResourceText.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.utils + +expect fun Any.readResourceText(path: String): String diff --git a/sample/mpp-library/src/commonTest/kotlin/PetstoreSearchResponse.kt b/sample/mpp-library/src/commonTest/resources/PetstoreSearchResponse.json similarity index 99% rename from sample/mpp-library/src/commonTest/kotlin/PetstoreSearchResponse.kt rename to sample/mpp-library/src/commonTest/resources/PetstoreSearchResponse.json index 994d5c7..ba4cbef 100644 --- a/sample/mpp-library/src/commonTest/kotlin/PetstoreSearchResponse.kt +++ b/sample/mpp-library/src/commonTest/resources/PetstoreSearchResponse.json @@ -1,8 +1,3 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -val petstoreSearchResponse = """ [ { "id": 9222968140497256000, @@ -3891,5 +3886,4 @@ val petstoreSearchResponse = """ ], "status": "available" } -] -""".trimIndent() \ No newline at end of file +] \ No newline at end of file diff --git a/sample/mpp-library/src/iosTest/kotlin/readResourceText.kt b/sample/mpp-library/src/iosTest/kotlin/readResourceText.kt new file mode 100644 index 0000000..1ee506e --- /dev/null +++ b/sample/mpp-library/src/iosTest/kotlin/readResourceText.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package tests.utils + +import platform.Foundation.NSBundle +import platform.Foundation.NSString +import platform.Foundation.stringWithContentsOfFile +import kotlin.test.assertNotNull + +actual fun Any.readResourceText(path: String): String { + val pathWithoutExtension: String = path.substringBeforeLast(".") + val extension = path.substringAfterLast(".") + val filePath: String? = NSBundle.mainBundle + .pathForResource("resources/$pathWithoutExtension", extension) + assertNotNull(filePath, "can't find file on path [$filePath]") + return NSString.stringWithContentsOfFile(filePath) as String +}