Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.18.0 #181

Merged
merged 30 commits into from
Jul 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
389d985
#149 added HttpClientEngine support to iOS, sample don't work
anton6tak Aug 25, 2021
42af4a9
#149 updated constraints for buttons
anton6tak Aug 26, 2021
94538b7
#149 add actual for JvmAndroid and fixes
anton6tak Aug 26, 2021
c50949f
#149 added opyrights
anton6tak Aug 26, 2021
951c311
#149 renamed buttons
anton6tak Aug 26, 2021
2481baa
#149 removed unnecessary printlns
anton6tak Aug 27, 2021
44cc044
#149 version update
anton6tak Aug 27, 2021
da3159b
#149 added websocket echo server app
anton6tak Aug 27, 2021
313bb0f
#149 added exception classes for send and receive message exceptions
anton6tak Aug 27, 2021
6b57208
#149 fixes, removed useless createHttpClientEngine without arguments
anton6tak Aug 27, 2021
0a69b15
#149 added websocket test button to android-sample app
anton6tak Aug 27, 2021
63e15dc
#149 updated reloadedWebsocket to use local echo server
anton6tak Aug 27, 2021
4cfdfe9
#149 localhosts for ios and android emulators
anton6tak Aug 27, 2021
bb46f4b
#149 fixed error on android: Cannot invoke setValue on a background t…
anton6tak Aug 27, 2021
ec05b15
#149 changed endless loop
anton6tak Aug 27, 2021
f475501
#149 plugin already on gradlePluginPortal
anton6tak Aug 27, 2021
25f3ab5
extra spaces problem
tunetab Jul 14, 2022
159aed3
#141 append headers
tunetab Jul 14, 2022
097b217
#141 test fix
tunetab Jul 14, 2022
3baa759
#141 fixed nullable possibility
tunetab Jul 14, 2022
f09ab09
copyright settings
Alex009 Jul 16, 2022
2491239
#141 copyright fix
Alex009 Jul 16, 2022
cfd5d88
Merge pull request #179 from tunetab/request_headers
Alex009 Jul 16, 2022
197df40
Merge branch 'develop' into #149-add-WebSockets-support-Ios
Alex009 Jul 16, 2022
56b4ccc
#149 fix detekt
Alex009 Jul 16, 2022
38af32d
#149 suppress invalid detekt
Alex009 Jul 16, 2022
8feb5ae
#176 add dynamic user agent feature
Alex009 Jul 16, 2022
c92058b
Merge pull request #158 from anton6tak/#149-add-WebSockets-support-Ios
Alex009 Jul 16, 2022
df002fc
Merge pull request #180 from icerockdev/#176-dynamic-user-agent
Alex009 Jul 16, 2022
d6a3e50
up version
Alex009 Jul 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .idea/copyright/IceRock.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/copyright/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ root build.gradle
```groovy
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}

dependencies {
classpath "dev.icerock.moko:network-generator:0.17.0"
classpath "dev.icerock.moko:network-generator:0.18.0"
}
}

Expand All @@ -53,9 +53,9 @@ project build.gradle
apply plugin: "dev.icerock.mobile.multiplatform-network-generator"

dependencies {
commonMainApi("dev.icerock.moko:network:0.17.0")
commonMainApi("dev.icerock.moko:network-bignum:0.17.0") // kbignum serializer
commonMainApi("dev.icerock.moko:network-errors:0.17.0") // moko-errors integration
commonMainApi("dev.icerock.moko:network:0.18.0")
commonMainApi("dev.icerock.moko:network-bignum:0.18.0") // kbignum serializer
commonMainApi("dev.icerock.moko:network-errors:0.18.0") // moko-errors integration
}
```

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mokoResourcesVersion = "0.18.0"
mokoMvvmVersion = "0.12.0"
mokoErrorsVersion = "0.6.0"
mokoTestVersion = "0.6.1"
mokoNetworkVersion = "0.17.0"
mokoNetworkVersion = "0.18.0"

# tests
espressoCoreVersion = "3.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ import io.ktor.http.content.TextContent
{{/hasFormParams}}
with(builder.headers) {
append("Accept", "application/json")
{{#headerParams}}
if ({{paramName}} != null)
append("{{baseName}}", {{paramName}})
{{/headerParams}}
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
}
{{/isMap}}
{{#isArray}}
{{#items}}{{>property_serializer}}{{/items}}.let {
{{#uniqueItems}}SetSerializer(it){{/uniqueItems}}
{{^uniqueItems}}ListSerializer(it){{/uniqueItems}}
}
{{#items}}{{>property_serializer}}{{/items}}.let {
{{#uniqueItems}}SetSerializer(it){{/uniqueItems}}
{{^uniqueItems}}ListSerializer(it){{/uniqueItems}}
}
{{/isArray}}
{{^containerType}}
{{dataType}}.serializer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network.features

import io.ktor.client.HttpClient
import io.ktor.client.features.HttpClientFeature
import io.ktor.client.request.HttpRequestPipeline
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
import io.ktor.util.AttributeKey

class DynamicUserAgent(
val agentProvider: () -> String?
) {
class Config(var agentProvider: () -> String? = { null })

companion object Feature : HttpClientFeature<Config, DynamicUserAgent> {
override val key: AttributeKey<DynamicUserAgent> = AttributeKey("DynamicUserAgent")

override fun prepare(block: Config.() -> Unit): DynamicUserAgent =
DynamicUserAgent(Config().apply(block).agentProvider)

override fun install(feature: DynamicUserAgent, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
feature.agentProvider()?.let { context.header(HttpHeaders.UserAgent, it) }
}
}
}
}
152 changes: 152 additions & 0 deletions network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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<String?> = CompletableDeferred()

private val webSocket: NSURLSessionWebSocketTask

private val _incoming = Channel<Frame>()
private val _outgoing = Channel<Frame>()
private val _closeReason = CompletableDeferred<CloseReason?>()

override val incoming: ReceiveChannel<Frame> = _incoming
override val outgoing: SendChannel<Frame> = _outgoing
override val closeReason: Deferred<CloseReason?> = _closeReason

@ExperimentalWebSocketExtensionApi
override val extensions: List<WebSocketExtension<*>>
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<WebSocketExtension<*>>) {
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* 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<HttpClientEngineCapability<*>>
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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): Htt
config.iosTimeoutIntervalForRequest?.let { setTimeoutIntervalForRequest(it) }
config.iosTimeoutIntervalForResource?.let { setTimeoutIntervalForResource(it) }
}
}
}.let { WSIosHttpClientEngine(it) }
}
3 changes: 2 additions & 1 deletion sample/android-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
android:name=".App"
android:label="moko-network test app"
android:theme="@style/Theme.AppCompat.DayNight"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning"
android:usesCleartextTraffic="true">

<activity android:name=".MainActivity">
<intent-filter>
Expand Down
Loading