├── sample
├── gradlew
├── ios-app
│ ├── src
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── AppDelegate.swift
│ │ ├── TestViewController.swift
│ │ ├── Info.plist
│ │ └── Resources
│ │ │ └── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ ├── TestProj.xcodeproj
│ │ └── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ ├── TestProj.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── Podfile.lock
│ └── Podfile
├── mpp-library
│ ├── src
│ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── icerockdev
│ │ │ │ └── library
│ │ │ │ └── emulatorLocalhost.kt
│ │ ├── commonTest
│ │ │ └── kotlin
│ │ │ │ ├── tests
│ │ │ │ └── utils
│ │ │ │ │ └── readResourceText.kt
│ │ │ │ ├── HeadersTest.kt
│ │ │ │ ├── createHttpClient.kt
│ │ │ │ ├── AllOfTest.kt
│ │ │ │ ├── PetApiTest.kt
│ │ │ │ ├── MapResponseTest.kt
│ │ │ │ ├── AnyTypeTest.kt
│ │ │ │ ├── FormDataTest.kt
│ │ │ │ ├── AnyOfTest.kt
│ │ │ │ └── EnumFallbackNullTest.kt
│ │ ├── commonMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── icerockdev
│ │ │ │ └── library
│ │ │ │ ├── emulatorLocalhost.kt
│ │ │ │ └── ExceptionStorage.kt
│ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── com
│ │ │ │ └── icerockdev
│ │ │ │ └── library
│ │ │ │ └── emulatorLocalhost.kt
│ │ ├── AnyType.yaml
│ │ ├── androidUnitTest
│ │ │ └── kotlin
│ │ │ │ └── readResourceText.kt
│ │ ├── mapResponse.yaml
│ │ ├── oneOf.yaml
│ │ ├── iosTest
│ │ │ └── kotlin
│ │ │ │ └── readResourceText.kt
│ │ ├── requestHeaders.yaml
│ │ ├── anyOf.yaml
│ │ ├── allOf.yaml
│ │ └── enumFallbackNull.yaml
│ ├── MultiPlatformLibrary.podspec
│ └── build.gradle.kts
├── android-app
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── logo.png
│ │ │ │ └── loading.xml
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── spinner_image.png
│ │ │ └── layout
│ │ │ │ └── activity_main.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── icerockdev
│ │ │ │ └── app
│ │ │ │ ├── App.kt
│ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── form-data-binary-server
│ ├── src
│ │ └── main
│ │ │ ├── resources
│ │ │ ├── application.conf
│ │ │ └── logback.xml
│ │ │ └── kotlin
│ │ │ └── com
│ │ │ └── icerockdev
│ │ │ └── server
│ │ │ └── Application.kt
│ └── build.gradle.kts
└── websocket-echo-server
│ ├── src
│ └── main
│ │ ├── resources
│ │ ├── application.conf
│ │ └── logback.xml
│ │ └── kotlin
│ │ └── com
│ │ └── icerockdev
│ │ └── server
│ │ └── Application.kt
│ └── build.gradle.kts
├── img
└── logo.png
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── network-generator
├── src
│ └── main
│ │ ├── resources
│ │ ├── META-INF
│ │ │ ├── services
│ │ │ │ └── org.openapitools.codegen.CodegenConfig
│ │ │ └── gradle-plugins
│ │ │ │ └── dev.icerock.mobile.multiplatform-network-generator-deprecated.properties
│ │ └── kotlin-ktor-client
│ │ │ ├── classes_modifiers.mustache
│ │ │ ├── model_doc.mustache
│ │ │ ├── enum_doc.mustache
│ │ │ ├── licenseInfo.mustache
│ │ │ ├── data_class_non_req_null_var.mustache
│ │ │ ├── property_serializer.mustache
│ │ │ ├── enum_class.mustache
│ │ │ ├── data_class_req_var.mustache
│ │ │ ├── model.mustache
│ │ │ ├── data_class_opt_var.mustache
│ │ │ ├── data_class_non_req_non_null_var.mustache
│ │ │ ├── class_doc.mustache
│ │ │ ├── data_class.mustache
│ │ │ ├── data_class_allof.mustache
│ │ │ ├── data_class_anyof.mustache
│ │ │ └── api_doc.mustache
│ │ └── kotlin
│ │ └── dev
│ │ └── icerock
│ │ └── moko
│ │ └── network
│ │ ├── OpenApiSchemaProcessor.kt
│ │ ├── SpecConfig.kt
│ │ ├── SpecInfo.kt
│ │ ├── OneOfOperatorProcessor.kt
│ │ ├── SchemaEnumNullProcessor.kt
│ │ ├── tasks
│ │ └── GenerateTask.kt
│ │ ├── MultiPlatformNetworkGeneratorDeprecatedPlugin.kt
│ │ ├── SchemaContext.kt
│ │ ├── MultiPlatformNetworkGeneratorPlugin.kt
│ │ └── ComposedSchemaProcessor.kt
├── gradle.properties
├── settings.gradle.kts
└── build.gradle.kts
├── .gitignore
├── network-errors
├── src
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── Dummy.kt
│ └── commonMain
│ │ ├── resources
│ │ └── MR
│ │ │ ├── base
│ │ │ └── strings.xml
│ │ │ └── ru
│ │ │ └── strings.xml
│ │ └── kotlin
│ │ └── dev
│ │ └── icerock
│ │ └── moko
│ │ └── network
│ │ └── errors
│ │ ├── NetworkErrorsTexts.kt
│ │ └── NetworkExceptionMappers.kt
└── build.gradle.kts
├── .idea
└── copyright
│ ├── profiles_settings.xml
│ └── IceRock.xml
├── network
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ ├── NetworkConnectionError.kt
│ │ │ ├── ParserUtils.kt
│ │ │ ├── LanguageProvider.kt
│ │ │ ├── nullable
│ │ │ ├── Nullable.kt
│ │ │ └── NullableSerializer.kt
│ │ │ ├── NetworkResponse.kt
│ │ │ ├── exceptions
│ │ │ ├── DataNotFitAnyOfSchema.kt
│ │ │ ├── DataNotFitOneOfSchema.kt
│ │ │ ├── ValidationException.kt
│ │ │ ├── ErrorException.kt
│ │ │ └── ResponseException.kt
│ │ │ ├── safeable
│ │ │ ├── Safeable.kt
│ │ │ └── SafeableSerializer.kt
│ │ │ ├── GMTDateExt.kt
│ │ │ ├── exceptionfactory
│ │ │ ├── ExceptionFactory.kt
│ │ │ ├── HttpExceptionFactory.kt
│ │ │ └── parser
│ │ │ │ ├── ErrorExceptionParser.kt
│ │ │ │ └── ValidationExceptionParser.kt
│ │ │ ├── isSSLException.kt
│ │ │ ├── plugins
│ │ │ ├── DynamicUserAgent.kt
│ │ │ ├── TokenPlugin.kt
│ │ │ ├── LanguagePlugin.kt
│ │ │ ├── ExceptionPlugin.kt
│ │ │ └── RefreshTokenPlugin.kt
│ │ │ ├── HttpExt.kt
│ │ │ ├── schemas
│ │ │ └── ComposedSchemaSerializer.kt
│ │ │ └── multipart
│ │ │ └── MultiPartContent.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ └── LanguageProvider.kt
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ ├── LanguageProvider.kt
│ │ │ ├── ParserUtils.kt
│ │ │ ├── ThrowableToNSErrorMapper.kt
│ │ │ ├── NetworkConnectionError.kt
│ │ │ ├── GMTDateExt.kt
│ │ │ └── isSSLException.kt
│ ├── commonJvmAndroid
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ ├── ParserUtils.kt
│ │ │ ├── GMTDateExt.kt
│ │ │ ├── isNetworkConnectionError.kt
│ │ │ └── isSSLException.kt
│ ├── androidMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ ├── LanguageProvider.kt
│ │ │ └── ParcelExt.kt
│ └── commonTest
│ │ └── kotlin
│ │ ├── TokenFeatureTest.kt
│ │ ├── NullableTest.kt
│ │ ├── LanguageFeatureTest.kt
│ │ ├── AllOfTest.kt
│ │ ├── SafeableTest.kt
│ │ └── OneOfTest.kt
└── build.gradle.kts
├── network-bignum
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── dev
│ └── icerock
│ └── moko
│ └── network
│ └── bignum
│ └── BigNumSerializer.kt
├── settings.gradle.kts
├── network-engine
├── src
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ └── HttpClientEngineConfig.kt
│ ├── commonJvmAndroid
│ │ └── kotlin
│ │ │ └── dev
│ │ │ └── icerock
│ │ │ └── moko
│ │ │ └── network
│ │ │ └── createHttpClientEngine.kt
│ └── iosMain
│ │ └── kotlin
│ │ └── dev
│ │ └── icerock
│ │ └── moko
│ │ └── network
│ │ └── createHttpClientEngine.kt
└── build.gradle.kts
├── gradle.properties
├── .github
└── workflows
│ ├── compilation-check.yml
│ └── publish.yml
├── CONTRIBUTING.md
└── gradlew.bat
/sample/gradlew:
--------------------------------------------------------------------------------
1 | ../gradlew
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icerockdev/moko-network/HEAD/img/logo.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icerockdev/moko-network/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/network-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig:
--------------------------------------------------------------------------------
1 | dev.icerock.moko.network.KtorCodegen
--------------------------------------------------------------------------------
/sample/ios-app/src/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/mpp-library/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/drawable/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icerockdev/moko-network/HEAD/sample/android-app/src/main/res/drawable/logo.png
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/classes_modifiers.mustache:
--------------------------------------------------------------------------------
1 | {{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}public {{/nonPublicApi}}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .settings
3 | .project
4 | .classpath
5 | .vscode
6 | .idea
7 | build
8 | *.iml
9 | Pods
10 | xcuserdata
11 | local.properties
12 | local.gradle
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/drawable-xxhdpi/spinner_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icerockdev/moko-network/HEAD/sample/android-app/src/main/res/drawable-xxhdpi/spinner_image.png
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/model_doc.mustache:
--------------------------------------------------------------------------------
1 | {{#models}}{{#model}}
2 | {{#isEnum}}{{>enum_doc}}{{/isEnum}}{{^isEnum}}{{>class_doc}}{{/isEnum}}
3 | {{/model}}{{/models}}
4 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/META-INF/gradle-plugins/dev.icerock.mobile.multiplatform-network-generator-deprecated.properties:
--------------------------------------------------------------------------------
1 | implementation-class=dev.icerock.moko.network.MultiPlatformNetworkGeneratorDeprecatedPlugin
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/enum_doc.mustache:
--------------------------------------------------------------------------------
1 | # {{classname}}
2 |
3 | ## Enum
4 |
5 | {{#allowableValues}}{{#enumVars}}
6 | * `{{name}}` (value: `{{{value}}}`)
7 | {{/enumVars}}{{/allowableValues}}
8 |
--------------------------------------------------------------------------------
/network-errors/src/iosMain/kotlin/Dummy.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | // required for produce `metadata/iosMain`
6 | internal val sDummyVar: Int? = null
7 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/form-data-binary-server/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | ktor {
2 | deployment {
3 | port = 8080
4 | port = ${?PORT}
5 | }
6 | application {
7 | modules = [ com.icerockdev.server.ApplicationKt.module ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/websocket-echo-server/src/main/resources/application.conf:
--------------------------------------------------------------------------------
1 | ktor {
2 | deployment {
3 | port = 8080
4 | port = ${?PORT}
5 | }
6 | application {
7 | modules = [ com.icerockdev.server.ApplicationKt.module ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/tests/utils/readResourceText.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package tests.utils
6 |
7 | expect fun Any.readResourceText(path: String): String
8 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | expect fun Throwable.isNetworkConnectionError(): Boolean
8 |
--------------------------------------------------------------------------------
/.idea/copyright/IceRock.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/emulatorLocalhost.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package com.icerockdev.library
8 |
9 | expect val emulatorLocalhost: String
10 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/iosMain/kotlin/com/icerockdev/library/emulatorLocalhost.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package com.icerockdev.library
8 |
9 | actual val emulatorLocalhost = "0.0.0.0"
10 |
--------------------------------------------------------------------------------
/sample/ios-app/TestProj.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/ParserUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 |
9 | expect fun String.formatToDate(parseFormat: String): GMTDate
10 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/androidMain/kotlin/com/icerockdev/library/emulatorLocalhost.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package com.icerockdev.library
8 |
9 | @Suppress("MayBeConst")
10 | actual val emulatorLocalhost = "10.0.2.2"
11 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import dev.icerock.moko.network.plugins.LanguagePlugin
8 |
9 | @Suppress("EmptyDefaultConstructor")
10 | expect class LanguageProvider() : LanguagePlugin.LanguageCodeProvider
11 |
--------------------------------------------------------------------------------
/sample/ios-app/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - MultiPlatformLibrary (0.1.0)
3 |
4 | DEPENDENCIES:
5 | - MultiPlatformLibrary (from `../mpp-library`)
6 |
7 | EXTERNAL SOURCES:
8 | MultiPlatformLibrary:
9 | :path: "../mpp-library"
10 |
11 | SPEC CHECKSUMS:
12 | MultiPlatformLibrary: 2a9f43df7bd018c32611a2087c1e2ef74847394c
13 |
14 | PODFILE CHECKSUM: 68b1a7b3453f9dbea0e91a6439f872724d5c91ce
15 |
16 | COCOAPODS: 1.11.3
17 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/licenseInfo.mustache:
--------------------------------------------------------------------------------
1 | /**
2 | * {{{appName}}}
3 | * {{{appDescription}}}
4 | *
5 | * {{#version}}OpenAPI spec version: {{{version}}}{{/version}}
6 | * {{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}}
7 | *
8 | * NOTE: This class is auto generated by the swagger code generator program.
9 | * https://github.com/swagger-api/swagger-codegen.git
10 | * Do not edit the class manually.
11 | */
--------------------------------------------------------------------------------
/sample/android-app/src/main/java/com/icerockdev/app/App.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.app
6 |
7 | import android.app.Application
8 | import com.icerockdev.library.initExceptionStorage
9 |
10 | class App : Application() {
11 | override fun onCreate() {
12 | super.onCreate()
13 | initExceptionStorage()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/nullable/Nullable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.nullable
6 |
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable(with = NullableSerializer::class)
10 | data class Nullable(val value: T?)
11 |
12 | fun T?.asNullable(): Nullable = Nullable(this)
13 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonMain/kotlin/com/icerockdev/library/ExceptionStorage.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.library
6 |
7 | import dev.icerock.moko.errors.mappers.ExceptionMappersStorage
8 | import dev.icerock.moko.network.errors.registerAllNetworkMappers
9 |
10 | fun initExceptionStorage() {
11 | ExceptionMappersStorage.registerAllNetworkMappers()
12 | }
13 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/OpenApiSchemaProcessor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.swagger.v3.oas.models.OpenAPI
8 | import io.swagger.v3.oas.models.media.Schema
9 |
10 | fun interface OpenApiSchemaProcessor {
11 | fun process(openApi: OpenAPI, schema: Schema<*>, context: SchemaContext): Schema<*>
12 | }
13 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/NetworkResponse.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.client.statement.HttpResponse
8 |
9 | data class NetworkResponse(
10 | val httpResponse: HttpResponse,
11 | private val bodyReader: suspend (HttpResponse) -> T
12 | ) {
13 | suspend fun body(): T = bodyReader(httpResponse)
14 | }
15 |
--------------------------------------------------------------------------------
/network-generator/gradle.properties:
--------------------------------------------------------------------------------
1 | moko.publish.name=MOKO network
2 | moko.publish.description=Network components with codegeneration of rest api for mobile (android & ios) Kotlin Multiplatform development
3 | moko.publish.repo.org=icerockdev
4 | moko.publish.repo.name=moko-network
5 | moko.publish.license=Apache-2.0
6 | moko.publish.developers=alex009|Aleksey Mikhailov|Aleksey.Mikhailov@icerockdev.com,Tetraquark|Vladislav Areshkin|vareshkin@icerockdev.com,Dorofeev|Andrey Dorofeev|adorofeev@icerockdev.com
7 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptions/DataNotFitAnyOfSchema.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptions
6 |
7 | import kotlinx.serialization.SerializationException
8 | import kotlinx.serialization.json.JsonElement
9 |
10 | class DataNotFitAnyOfSchema(
11 | val data: JsonElement,
12 | val causes: List
13 | ) : SerializationException()
14 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptions/DataNotFitOneOfSchema.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptions
6 |
7 | import kotlinx.serialization.SerializationException
8 | import kotlinx.serialization.json.JsonElement
9 |
10 | class DataNotFitOneOfSchema(
11 | val data: JsonElement,
12 | val results: List>
13 | ) : SerializationException()
14 |
--------------------------------------------------------------------------------
/sample/websocket-echo-server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_non_req_null_var.mustache:
--------------------------------------------------------------------------------
1 | {{#description}}
2 | /* {{{description}}} */
3 | {{/description}}
4 | {{#isDecimal}}@Serializable(with = BigNumSerializer::class) {{/isDecimal}}
5 | @SerialName("{{{baseName}}}")
6 | val {{{name}}}: dev.icerock.moko.network.nullable.Nullable<{{#isEnum}}{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{nameInCamelCase}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}>? = null
--------------------------------------------------------------------------------
/network/src/jvmMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import dev.icerock.moko.network.plugins.LanguagePlugin
8 | import java.util.Locale
9 |
10 | actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider {
11 | override fun getLanguageCode(): String? {
12 | return Locale.getDefault().displayLanguage
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/form-data-binary-server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/AnyType.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /dynamic:
7 | get:
8 | responses:
9 | '200':
10 | description: Updated
11 | content:
12 | application/json:
13 | schema:
14 | $ref: '#/components/schemas/Resp'
15 | components:
16 | schemas:
17 | Resp:
18 | type: object
19 | properties:
20 | anyProp: {}
21 | anyList:
22 | type: array
23 | items: {}
24 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/property_serializer.mustache:
--------------------------------------------------------------------------------
1 | {{#isMap}}
2 | {{#additionalProperties}}{{>property_serializer}}{{/additionalProperties}}.let {
3 | MapSerializer(keySerializer = String.serializer(), valueSerializer = it)
4 | }
5 | {{/isMap}}
6 | {{#isArray}}
7 | {{#items}}{{>property_serializer}}{{/items}}.let {
8 | {{#uniqueItems}}SetSerializer(it){{/uniqueItems}}
9 | {{^uniqueItems}}ListSerializer(it){{/uniqueItems}}
10 | }
11 | {{/isArray}}
12 | {{^containerType}}
13 | {{dataType}}.serializer()
14 | {{/containerType}}
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/safeable/Safeable.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.safeable
6 |
7 | import kotlinx.serialization.Serializable
8 |
9 | @Serializable(with = SafeableSerializer::class)
10 | data class Safeable(val value: T?)
11 |
12 | fun T?.asSafeable(): Safeable = Safeable(this)
13 |
14 | fun Collection>.extractSafeables(): Collection = map { it.value }
15 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/GMTDateExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 |
9 | /**
10 | * Parses string from the beginning of the given string to produce a date by the [format].
11 | *
12 | * @throws IllegalArgumentException if the date [format] is incorrect.
13 | */
14 | expect fun String.toDate(format: String): GMTDate
15 |
16 | expect fun GMTDate.toString(format: String): String
17 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/enum_class.mustache:
--------------------------------------------------------------------------------
1 | import kotlinx.serialization.SerialName
2 |
3 | /**
4 | * {{{description}}}
5 | * Values: {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
6 | */
7 | @Serializable
8 | {{#nonPublicApi}}internal {{/nonPublicApi}}enum class {{classname}}() {
9 | {{#allowableValues}}{{#enumVars}}
10 | @SerialName({{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}})
11 | {{name}}{{^-last}},{{/-last}}{{#-last}};{{/-last}}
12 | {{/enumVars}}{{/allowableValues}}
13 | }
14 |
--------------------------------------------------------------------------------
/sample/form-data-binary-server/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | id("application")
4 | }
5 |
6 | group = "com.example"
7 | version = "0.0.1"
8 |
9 | application {
10 | mainClass.set("io.ktor.server.netty.EngineMain")
11 | }
12 |
13 | java {
14 | sourceCompatibility = JavaVersion.VERSION_1_8
15 | targetCompatibility = JavaVersion.VERSION_1_8
16 | }
17 |
18 | dependencies {
19 | implementation("io.ktor:ktor-server-netty:2.2.1")
20 | implementation("io.ktor:ktor-server-core:2.2.1")
21 | implementation("ch.qos.logback:logback-classic:1.2.11")
22 | }
23 |
--------------------------------------------------------------------------------
/sample/ios-app/src/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import MultiPlatformLibrary
6 | import UIKit
7 |
8 | @UIApplicationMain
9 | class AppDelegate: NSObject, UIApplicationDelegate {
10 |
11 | var window: UIWindow?
12 |
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
14 | ExceptionStorageKt.doInitExceptionStorage()
15 |
16 | return true
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/drawable/loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
8 |
9 |
10 | -
11 |
15 |
16 |
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import dev.icerock.moko.network.plugins.LanguagePlugin
8 | import platform.Foundation.NSLocale
9 | import platform.Foundation.currentLocale
10 | import platform.Foundation.languageCode
11 |
12 | actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider {
13 | override fun getLanguageCode(): String {
14 | return NSLocale.currentLocale.languageCode
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/ParserUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 | import java.text.SimpleDateFormat
9 | import java.util.Locale
10 | import java.util.TimeZone
11 |
12 | actual fun String.formatToDate(parseFormat: String): GMTDate {
13 | val date = SimpleDateFormat(parseFormat, Locale.ROOT).apply {
14 | timeZone = TimeZone.getTimeZone("GMT")
15 | }.parse(this)
16 | return GMTDate(date.time)
17 | }
18 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptionfactory/ExceptionFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptionfactory
6 |
7 | import dev.icerock.moko.network.exceptions.ResponseException
8 | import io.ktor.client.request.HttpRequest
9 | import io.ktor.client.statement.HttpResponse
10 |
11 | interface ExceptionFactory {
12 | fun createException(
13 | request: HttpRequest,
14 | response: HttpResponse,
15 | responseBody: String?
16 | ): ResponseException
17 | }
18 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptions/ValidationException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptions
6 |
7 | import io.ktor.client.request.HttpRequest
8 | import io.ktor.client.statement.HttpResponse
9 |
10 | class ValidationException(
11 | request: HttpRequest,
12 | response: HttpResponse,
13 | message: String,
14 | val errors: List
15 | ) : ResponseException(request, response, message) {
16 | data class Error(val field: String, val message: String)
17 | }
18 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/HeadersTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import io.ktor.client.HttpClient
6 | import io.ktor.client.engine.mock.respondOk
7 | import kotlin.test.Test
8 | import kotlin.test.assertTrue
9 |
10 | class HeadersTest {
11 | private lateinit var httpClient: HttpClient
12 |
13 | @Test
14 | fun `auth test`() {
15 | httpClient = createMockClient { request ->
16 | assertTrue { request.headers.contains("Authorization") }
17 | respondOk()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/network/src/androidMain/kotlin/dev/icerock/moko/network/LanguageProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import android.content.res.Resources
8 | import androidx.core.os.ConfigurationCompat
9 | import dev.icerock.moko.network.plugins.LanguagePlugin
10 |
11 | actual class LanguageProvider : LanguagePlugin.LanguageCodeProvider {
12 | override fun getLanguageCode(): String? {
13 | return ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)?.language
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/sample/websocket-echo-server/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("kotlin")
3 | id("application")
4 | }
5 |
6 | group = "com.example"
7 | version = "0.0.1"
8 |
9 | application {
10 | mainClass.set("io.ktor.server.netty.EngineMain")
11 | }
12 |
13 | java {
14 | sourceCompatibility = JavaVersion.VERSION_1_8
15 | targetCompatibility = JavaVersion.VERSION_1_8
16 | }
17 |
18 | dependencies {
19 | implementation("io.ktor:ktor-server-netty:1.5.1")
20 | implementation("io.ktor:ktor-server-core:1.5.1")
21 | implementation("io.ktor:ktor-websockets:1.5.1")
22 | implementation("ch.qos.logback:logback-classic:1.2.9")
23 | }
24 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptions/ErrorException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptions
6 |
7 | import io.ktor.client.request.HttpRequest
8 | import io.ktor.client.statement.HttpResponse
9 |
10 | class ErrorException(
11 | request: HttpRequest,
12 | response: HttpResponse,
13 | val code: Int,
14 | val description: String?
15 | ) : ResponseException(request, response, description.orEmpty()) {
16 | override val message: String?
17 | get() = description ?: super.message
18 | }
19 |
--------------------------------------------------------------------------------
/network-bignum/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("dev.icerock.moko.gradle.detekt")
8 | id("dev.icerock.moko.gradle.publication")
9 | id("dev.icerock.moko.gradle.stub.javadoc")
10 | }
11 |
12 | android {
13 | namespace = "dev.icerock.moko.network.bignum"
14 | }
15 |
16 | kotlin {
17 | jvm()
18 | }
19 |
20 | dependencies {
21 | commonMainImplementation(libs.kotlinSerialization)
22 | commonMainApi(libs.kbignum)
23 |
24 | commonMainImplementation(projects.network)
25 | }
26 |
--------------------------------------------------------------------------------
/network-generator/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 | enableFeaturePreview("VERSION_CATALOGS")
5 |
6 | pluginManagement {
7 | repositories {
8 | mavenCentral()
9 | google()
10 |
11 | gradlePluginPortal()
12 | }
13 | }
14 |
15 | dependencyResolutionManagement {
16 | repositories {
17 | mavenCentral()
18 | google()
19 | }
20 |
21 | versionCatalogs {
22 | create("libs") {
23 | from(files("../gradle/libs.versions.toml"))
24 | }
25 | }
26 | }
27 |
28 | rootProject.name = "network-generator"
29 |
--------------------------------------------------------------------------------
/sample/ios-app/Podfile:
--------------------------------------------------------------------------------
1 | source 'https://cdn.cocoapods.org/'
2 |
3 | # ignore all warnings from all pods
4 | inhibit_all_warnings!
5 |
6 | use_frameworks!
7 | platform :ios, '11.0'
8 |
9 | # workaround for https://github.com/CocoaPods/CocoaPods/issues/8073
10 | # need for correct invalidate of cache MultiPlatformLibrary.framework
11 | install! 'cocoapods', :disable_input_output_paths => true
12 |
13 | target 'TestProj' do
14 | # MultiPlatformLibrary
15 | # для корректной установки фреймворка нужно сначала скомпилировать котлин библиотеку - compile-kotlin-framework.sh (в корневой директории репозитория)
16 | pod 'MultiPlatformLibrary', :path => '../mpp-library'
17 | end
18 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/SpecConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import org.gradle.api.NamedDomainObjectContainer
8 | import org.gradle.api.model.ObjectFactory
9 | import javax.inject.Inject
10 |
11 | open class SpecConfig @Inject constructor(objectFactory: ObjectFactory) {
12 |
13 | internal val specs: NamedDomainObjectContainer =
14 | objectFactory.domainObjectContainer(SpecInfo::class.java)
15 |
16 | fun spec(name: String, setup: SpecInfo.() -> Unit) {
17 | specs.create(name).setup()
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/androidUnitTest/kotlin/readResourceText.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package tests.utils
6 |
7 | import java.io.InputStream
8 | import kotlin.test.assertNotNull
9 |
10 | actual fun Any.readResourceText(path: String): String {
11 | val classLoader: ClassLoader? = this.javaClass.classLoader
12 | assertNotNull(classLoader, "can't get classLoader of $this")
13 | val resource: InputStream? = classLoader.getResourceAsStream(path)
14 | assertNotNull(resource, "can't find resource with path [$path]")
15 | return resource
16 | .bufferedReader()
17 | .readText()
18 | }
19 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/mapResponse.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /dynamic:
7 | get:
8 | responses:
9 | '200':
10 | description: Updated
11 | content:
12 | application/json:
13 | schema:
14 | $ref: '#/components/schemas/DogsByGroup'
15 | components:
16 | schemas:
17 | DogsByGroup:
18 | type: object
19 | additionalProperties:
20 | type: array
21 | items:
22 | $ref: '#/components/schemas/Dog'
23 | Dog:
24 | type: object
25 | properties:
26 | bark:
27 | type: boolean
28 | breed:
29 | type: string
30 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/isSSLException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | expect fun Throwable.isSSLException(): Boolean
10 |
11 | expect fun Throwable.getSSLExceptionType(): SSLExceptionType?
12 |
13 | enum class SSLExceptionType {
14 | SecureConnectionFailed,
15 | ServerCertificateHasBadDate,
16 | ServerCertificateUntrusted,
17 | ServerCertificateHasUnknownRoot,
18 | ServerCertificateNotYetValid,
19 | ClientCertificateRejected,
20 | ClientCertificateRequired,
21 | CannotLoadFromNetwork
22 | }
23 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_req_var.mustache:
--------------------------------------------------------------------------------
1 | {{#description}}
2 | /* {{{description}}} */
3 | {{/description}}
4 | {{#isDecimal}}@Serializable(with = BigNumSerializer::class) {{/isDecimal}}
5 | @SerialName("{{{baseName}}}")
6 | val {{{name}}}: {{#isEnum}}{{#isArray}}kotlin.collections.List<{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{{nameInCamelCase}}}>{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}{{/isArray}}{{^isArray}}{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{nameInCamelCase}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}{{/isArray}}{{#isNullable}}?{{/isNullable}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{#isNullable}}?{{/isNullable}}{{/isEnum}}
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/ParserUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 | import platform.Foundation.NSDateFormatter
9 | import platform.Foundation.timeIntervalSince1970
10 |
11 | actual fun String.formatToDate(parseFormat: String): GMTDate {
12 | val formatter = NSDateFormatter()
13 | formatter.dateFormat = parseFormat
14 | return GMTDate(
15 | formatter.dateFromString(this)?.timeIntervalSince1970?.toLong()
16 | ?: throw IllegalArgumentException("Failed, to parse $this for format $parseFormat")
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/sample/android-app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /opt/android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | enableFeaturePreview("VERSION_CATALOGS")
6 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
7 |
8 | dependencyResolutionManagement {
9 | repositories {
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
15 | rootProject.name = "moko-network"
16 |
17 | includeBuild("network-generator")
18 |
19 | include(":network")
20 | include(":network-errors")
21 | include(":network-bignum")
22 | include(":network-engine")
23 |
24 | include(":sample:android-app")
25 | include(":sample:websocket-echo-server")
26 | include(":sample:form-data-binary-server")
27 | include(":sample:mpp-library")
28 |
--------------------------------------------------------------------------------
/network-engine/src/commonMain/kotlin/dev/icerock/moko/network/HttpClientEngineConfig.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.client.engine.HttpClientEngine
8 |
9 | class HttpClientEngineConfig {
10 | var androidConnectTimeoutSeconds: Long? = null
11 | var androidCallTimeoutSeconds: Long? = null
12 | var androidReadTimeoutSeconds: Long? = null
13 | var androidWriteTimeoutSeconds: Long? = null
14 | var iosTimeoutIntervalForRequest: Double? = null
15 | var iosTimeoutIntervalForResource: Double? = null
16 | }
17 |
18 | expect fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit = {}): HttpClientEngine
19 |
--------------------------------------------------------------------------------
/network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/GMTDateExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 | import java.text.ParseException
9 | import java.text.SimpleDateFormat
10 | import java.util.Date
11 | import java.util.Locale
12 |
13 | actual fun String.toDate(format: String) = try {
14 | GMTDate(SimpleDateFormat(format, Locale.getDefault()).parse(this).time)
15 | } catch (parseException: ParseException) {
16 | throw IllegalArgumentException("Parsing error: the date format is incorrect")
17 | }
18 |
19 | actual fun GMTDate.toString(format: String): String =
20 | SimpleDateFormat(format, Locale.getDefault()).format(Date(timestamp))
21 |
--------------------------------------------------------------------------------
/network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/isNetworkConnectionError.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import java.net.ConnectException
10 | import java.net.SocketException
11 | import java.net.SocketTimeoutException
12 | import java.net.UnknownHostException
13 | import java.util.concurrent.TimeoutException
14 |
15 | actual fun Throwable.isNetworkConnectionError(): Boolean {
16 | return when (this) {
17 | is ConnectException,
18 | is SocketException,
19 | is SocketTimeoutException,
20 | is TimeoutException,
21 | is UnknownHostException -> true
22 | else -> false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/model.mustache:
--------------------------------------------------------------------------------
1 | {{>licenseInfo}}
2 | package {{modelPackage}}
3 |
4 | {{#imports}}import {{import}}
5 | {{/imports}}
6 | import kotlinx.serialization.Serializable
7 |
8 | {{#models}}
9 | {{#model}}
10 | {{#isEnum}}{{>enum_class}}{{/isEnum}}
11 | {{#isAlias}}typealias {{classname}} = {{dataType}}{{/isAlias}}
12 | {{#vendorExtensions.x-allOfGeneration}}{{>data_class_allof}}{{/vendorExtensions.x-allOfGeneration}}
13 | {{#vendorExtensions.x-anyOfGeneration}}{{>data_class_anyof}}{{/vendorExtensions.x-anyOfGeneration}}
14 |
15 | {{^isEnum}}{{^isAlias}}{{^vendorExtensions.x-allOfGeneration}}{{^vendorExtensions.x-anyOfGeneration}}{{>data_class}}{{/vendorExtensions.x-anyOfGeneration}}{{/vendorExtensions.x-allOfGeneration}}{{/isAlias}}{{/isEnum}}
16 | {{/model}}
17 | {{/models}}
18 |
--------------------------------------------------------------------------------
/sample/android-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.android.application")
7 | id("dev.icerock.moko.gradle.detekt")
8 | id("kotlin-kapt")
9 | }
10 |
11 | android {
12 | buildFeatures.dataBinding = true
13 |
14 | defaultConfig {
15 | applicationId = "dev.icerock.moko.samples.network"
16 |
17 | multiDexEnabled = true
18 | versionCode = 1
19 | versionName = "0.1.0"
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation(libs.coreKtx)
25 | implementation(libs.appCompat)
26 | implementation(libs.mokoMvvmDataBinding)
27 | implementation(libs.multidex)
28 | implementation(projects.sample.mppLibrary)
29 | }
30 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/oneOf.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /pets:
7 | patch:
8 | requestBody:
9 | content:
10 | application/json:
11 | schema:
12 | oneOf:
13 | - $ref: '#/components/schemas/Cat'
14 | - $ref: '#/components/schemas/Dog'
15 | responses:
16 | '200':
17 | description: Updated
18 | components:
19 | schemas:
20 | Dog:
21 | type: object
22 | properties:
23 | bark:
24 | type: boolean
25 | breed:
26 | type: string
27 | enum: [Dingo, Husky, Retriever, Shepherd]
28 | Cat:
29 | type: object
30 | properties:
31 | hunts:
32 | type: boolean
33 | age:
34 | type: integer
--------------------------------------------------------------------------------
/sample/mpp-library/src/iosTest/kotlin/readResourceText.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package tests.utils
6 |
7 | import platform.Foundation.NSBundle
8 | import platform.Foundation.NSString
9 | import platform.Foundation.stringWithContentsOfFile
10 | import kotlin.test.assertNotNull
11 |
12 | actual fun Any.readResourceText(path: String): String {
13 | val pathWithoutExtension: String = path.substringBeforeLast(".")
14 | val extension = path.substringAfterLast(".")
15 | val filePath: String? = NSBundle.mainBundle
16 | .pathForResource("resources/$pathWithoutExtension", extension)
17 | assertNotNull(filePath, "can't find file on path [$filePath]")
18 | return NSString.stringWithContentsOfFile(filePath) as String
19 | }
20 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_opt_var.mustache:
--------------------------------------------------------------------------------
1 | {{#description}}
2 | /* {{{description}}} */
3 | {{/description}}
4 | {{#isDecimal}}@Serializable(with = BigNumSerializer::class) {{/isDecimal}}
5 | @SerialName("{{{baseName}}}")
6 | val {{{name}}}: {{#isEnum}}{{#isArray}}kotlin.collections.List<{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{{nameInCamelCase}}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}>{{/isArray}}{{^isArray}}{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{{nameInCamelCase}}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{#defaultvalue}}{{defaultvalue}}{{#isEnum}}{{#isEnumFallbackNull}}.asSafeable(){{/isEnumFallbackNull}}{{/isEnum}}{{/defaultvalue}}{{^defaultvalue}}null{{/defaultvalue}}
--------------------------------------------------------------------------------
/network-errors/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("dev.icerock.mobile.multiplatform-resources")
8 | id("dev.icerock.moko.gradle.detekt")
9 | id("dev.icerock.moko.gradle.publication")
10 | id("dev.icerock.moko.gradle.stub.javadoc")
11 | }
12 |
13 | android {
14 | namespace = "dev.icerock.moko.network.errors"
15 | }
16 |
17 | dependencies {
18 | commonMainImplementation(libs.kotlinSerialization)
19 |
20 | commonMainApi(libs.mokoErrors)
21 | commonMainApi(libs.mokoResources)
22 |
23 | commonMainImplementation(projects.network)
24 | }
25 |
26 | multiplatformResources {
27 | multiplatformResourcesPackage = "dev.icerock.moko.network.errors"
28 | }
29 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_non_req_non_null_var.mustache:
--------------------------------------------------------------------------------
1 | {{#description}}
2 | /* {{{description}}} */
3 | {{/description}}
4 | {{#isDecimal}}@Serializable(with = BigNumSerializer::class) {{/isDecimal}}
5 | @SerialName("{{{baseName}}}")
6 | val {{{name}}}: {{#isEnum}}{{#isArray}}kotlin.collections.List<{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{{nameInCamelCase}}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}>{{/isArray}}{{^isArray}}{{#isEnumFallbackNull}}Safeable<{{/isEnumFallbackNull}}{{classname}}.{{nameInCamelCase}}{{#isEnumFallbackNull}}>{{/isEnumFallbackNull}}{{/isArray}}{{/isEnum}}{{^isEnum}}{{{datatype}}}{{/isEnum}}{{#defaultValue}} = {{{defaultValue}}}{{#isEnum}}{{#isEnumFallbackNull}}.asSafeable(){{/isEnumFallbackNull}}{{/isEnum}}{{/defaultValue}}{{^defaultValue}}? = null{{/defaultValue}}
--------------------------------------------------------------------------------
/network/src/androidMain/kotlin/dev/icerock/moko/network/ParcelExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import android.os.Parcel
8 | import io.ktor.util.date.GMTDate
9 |
10 | fun Parcel.writeGMTDate(date: GMTDate) {
11 | writeLong(date.timestamp)
12 | }
13 |
14 | fun Parcel.readGMTDate(): GMTDate {
15 | return GMTDate(timestamp = readLong())
16 | }
17 |
18 | fun Parcel.writeGMTDateSafe(value: GMTDate?) {
19 | if (value == null) {
20 | writeByte(0)
21 | } else {
22 | writeByte(1)
23 | writeGMTDate(value)
24 | }
25 | }
26 |
27 | fun Parcel.readGMTDateSafe(): GMTDate? {
28 | return if (readByte() == 1.toByte()) {
29 | readGMTDate()
30 | } else {
31 | null
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/requestHeaders.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /user:
7 | get:
8 | tags:
9 | - Auth
10 | summary: sign in with token
11 | operationId: 'auth'
12 | security:
13 | - bearerAuth: [ ]
14 | parameters:
15 | - name: Authorization
16 | in: header
17 | required: true
18 | schema:
19 | type: string
20 | responses:
21 | '200':
22 | description: 'Ok'
23 | content:
24 | application/json:
25 | schema:
26 | $ref: '#/components/schemas/UserInfoResponse'
27 | components:
28 | schemas:
29 | UserInfoResponse:
30 | description: 'authorization response'
31 | required:
32 | - login
33 | properties:
34 | login:
35 | type: string
36 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/class_doc.mustache:
--------------------------------------------------------------------------------
1 | # {{classname}}
2 |
3 | ## Properties
4 | Name | Type | Description | Notes
5 | ------------ | ------------- | ------------- | -------------
6 | {{#vars}}**{{name}}** | {{#isEnum}}[**inline**](#{{datatypeWithEnum}}){{/isEnum}}{{^isEnum}}{{#isPrimitiveType}}**{{datatype}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{datatype}}**]({{complexType}}.md){{/isPrimitiveType}}{{/isEnum}} | {{description}} | {{^required}} [optional]{{/required}}{{#readOnly}} [readonly]{{/readOnly}}
7 | {{/vars}}
8 | {{#vars}}{{#isEnum}}
9 |
10 | {{!NOTE: see java's resources "pojo_doc.mustache" once enums are fully implemented}}
11 | ## Enum: {{baseName}}
12 | Name | Value
13 | ---- | -----{{#allowableValues}}
14 | {{name}} | {{#values}}{{.}}{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}
15 | {{/isEnum}}{{/vars}}
16 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/SpecInfo.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
8 | import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
9 | import java.io.File
10 |
11 | open class SpecInfo(val name: String) {
12 | var inputSpec: File? = null
13 | var sourceSet: String = KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME
14 | var packageName: String? = "dev.icerock.moko.network.generated"
15 | var isInternal = true
16 | var isOpen = false
17 | var filterTags: List = listOf()
18 | var enumFallbackNull = false
19 | internal var configureTask: (GenerateTask.() -> Unit)? = null
20 |
21 | fun configureTask(block: GenerateTask.() -> Unit) { configureTask = block }
22 | }
23 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4096m
2 | org.gradle.configureondemand=false
3 | org.gradle.parallel=true
4 |
5 | kotlin.code.style=official
6 |
7 | kotlin.mpp.stability.nowarn=true
8 | kotlin.mpp.androidSourceSetLayoutVersion=2
9 |
10 | android.useAndroidX=true
11 |
12 | mobile.multiplatform.iosTargetWarning=false
13 |
14 | xcodeproj=./sample/ios-app
15 |
16 | moko.android.targetSdk=33
17 | moko.android.compileSdk=33
18 | moko.android.minSdk=16
19 |
20 | moko.publish.name=MOKO network
21 | moko.publish.description=Network components with codegeneration of rest api for mobile (android & ios) Kotlin Multiplatform development
22 | moko.publish.repo.org=icerockdev
23 | moko.publish.repo.name=moko-network
24 | moko.publish.license=Apache-2.0
25 | moko.publish.developers=alex009|Aleksey Mikhailov|Aleksey.Mikhailov@icerockdev.com,Tetraquark|Vladislav Areshkin|vareshkin@icerockdev.com,Dorofeev|Andrey Dorofeev|adorofeev@icerockdev.com
26 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/compilation-check.yml:
--------------------------------------------------------------------------------
1 | name: KMP library compilation check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | - develop
8 |
9 | jobs:
10 | build:
11 | runs-on: macOS-11
12 |
13 | steps:
14 | - uses: actions/checkout@v1
15 | - name: Set up JDK 11
16 | uses: actions/setup-java@v1
17 | with:
18 | java-version: 11
19 | - name: Check plugin
20 | run: ./gradlew -p network-generator build publishToMavenLocal
21 | - name: Check runtime
22 | run: ./gradlew build publishToMavenLocal syncMultiPlatformLibraryDebugFrameworkIosX64
23 | - name: Install pods
24 | run: cd sample/ios-app && pod install
25 | - name: Check iOS
26 | run: cd sample/ios-app && set -o pipefail && xcodebuild -scheme TestProj -workspace TestProj.xcworkspace -configuration Debug -sdk iphonesimulator -arch x86_64 build CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty
27 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/OneOfOperatorProcessor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.swagger.v3.oas.models.OpenAPI
8 | import io.swagger.v3.oas.models.media.ComposedSchema
9 | import io.swagger.v3.oas.models.media.Schema
10 |
11 | /**
12 | * OpenApi schema processor that replace oneOf operator to the temporary type [propertyNewType].
13 | */
14 | internal class OneOfOperatorProcessor(
15 | private val propertyNewType: String
16 | ) : OpenApiSchemaProcessor {
17 |
18 | @Suppress("ReturnCount")
19 | override fun process(openApi: OpenAPI, schema: Schema<*>, context: SchemaContext): Schema<*> {
20 | if (schema !is ComposedSchema) return schema
21 | if (schema.oneOf.isNullOrEmpty()) return schema
22 |
23 | return Schema().apply {
24 | type = propertyNewType
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/SchemaEnumNullProcessor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.swagger.v3.oas.models.OpenAPI
8 | import io.swagger.v3.oas.models.media.Schema
9 |
10 | /**
11 | * OpenApi schema processor that removes all [null] items from enum fields of the schema.
12 | */
13 | internal class SchemaEnumNullProcessor : OpenApiSchemaProcessor {
14 | override fun process(openApi: OpenAPI, schema: Schema<*>, context: SchemaContext): Schema<*> {
15 | val schemaProperties = schema.properties ?: return schema
16 |
17 | schemaProperties.forEach { (_, propSchema) ->
18 | val enumField = propSchema.enum
19 | if (enumField != null && enumField.isNotEmpty()) {
20 | propSchema.enum = enumField.filterNotNull()
21 | }
22 | }
23 |
24 | return schema
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/ThrowableToNSErrorMapper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2023 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("MaximumLineLength")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import platform.Foundation.NSError
10 | import kotlin.native.concurrent.AtomicReference
11 |
12 | object ThrowableToNSErrorMapper : (Throwable) -> NSError? {
13 | private val mapperRef: AtomicReference<((Throwable) -> NSError?)?> = AtomicReference(null)
14 |
15 | override fun invoke(throwable: Throwable): NSError? {
16 | @Suppress("MaxLineLength")
17 | return requireNotNull(mapperRef.value) {
18 | "please setup ThrowableToNSErrorMapper by call ThrowableToNSErrorMapper.setup() in iosMain or use dev.icerock.moko.network.createHttpClientEngine"
19 | }.invoke(throwable)
20 | }
21 |
22 | fun setup(block: (Throwable) -> NSError?) {
23 | mapperRef.value = block
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/network-engine/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import io.ktor.client.engine.HttpClientEngine
10 | import io.ktor.client.engine.okhttp.OkHttp
11 | import java.util.concurrent.TimeUnit
12 |
13 | actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): HttpClientEngine {
14 | val config = HttpClientEngineConfig().also(block)
15 | return OkHttp.create {
16 | this.config {
17 | config.androidConnectTimeoutSeconds?.let { connectTimeout(it, TimeUnit.SECONDS) }
18 | config.androidCallTimeoutSeconds?.let { callTimeout(it, TimeUnit.SECONDS) }
19 | config.androidReadTimeoutSeconds?.let { readTimeout(it, TimeUnit.SECONDS) }
20 | config.androidWriteTimeoutSeconds?.let { writeTimeout(it, TimeUnit.SECONDS) }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptions/ResponseException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptions
6 |
7 | import io.ktor.client.request.HttpRequest
8 | import io.ktor.client.statement.HttpResponse
9 | import io.ktor.http.HttpStatusCode
10 |
11 | open class ResponseException(
12 | val request: HttpRequest,
13 | val response: HttpResponse,
14 | val responseMessage: String
15 | ) : Exception("Request: ${request.url}; Response: $responseMessage [$response.status.value]") {
16 |
17 | val httpStatusCode: Int
18 | get() = response.status.value
19 |
20 | val isUnauthorized: Boolean
21 | get() = httpStatusCode == HttpStatusCode.Unauthorized.value
22 |
23 | val isAccessDenied: Boolean
24 | get() = httpStatusCode == HttpStatusCode.Forbidden.value
25 |
26 | val isNotFound: Boolean
27 | get() = httpStatusCode == HttpStatusCode.NotFound.value
28 | }
29 |
--------------------------------------------------------------------------------
/network-engine/src/iosMain/kotlin/dev/icerock/moko/network/createHttpClientEngine.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import io.ktor.client.engine.HttpClientEngine
10 | import io.ktor.client.engine.darwin.Darwin
11 | import io.ktor.client.engine.darwin.DarwinHttpRequestException
12 |
13 | actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): HttpClientEngine {
14 | // configure darwin throwable mapper
15 | ThrowableToNSErrorMapper.setup { (it as? DarwinHttpRequestException)?.origin }
16 | // configure darwin engine
17 | val config = HttpClientEngineConfig().also(block)
18 | return Darwin.create {
19 | this.configureSession {
20 | config.iosTimeoutIntervalForRequest?.let { setTimeoutIntervalForRequest(it) }
21 | config.iosTimeoutIntervalForResource?.let { setTimeoutIntervalForResource(it) }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/network/src/commonJvmAndroid/kotlin/dev/icerock/moko/network/isSSLException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import javax.net.ssl.SSLException
10 | import javax.net.ssl.SSLHandshakeException
11 | import javax.net.ssl.SSLKeyException
12 | import javax.net.ssl.SSLPeerUnverifiedException
13 | import javax.net.ssl.SSLProtocolException
14 |
15 | actual fun Throwable.isSSLException(): Boolean {
16 | return this is SSLException
17 | }
18 |
19 | actual fun Throwable.getSSLExceptionType(): SSLExceptionType? {
20 | return when (this) {
21 | is SSLHandshakeException -> SSLExceptionType.ServerCertificateUntrusted
22 | is SSLKeyException -> SSLExceptionType.ClientCertificateRejected
23 | is SSLPeerUnverifiedException -> SSLExceptionType.ClientCertificateRequired
24 | is SSLProtocolException -> SSLExceptionType.SecureConnectionFailed
25 | is SSLException -> SSLExceptionType.SecureConnectionFailed
26 | else -> null
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/network-bignum/src/commonMain/kotlin/dev/icerock/moko/network/bignum/BigNumSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.bignum
6 |
7 | import com.soywiz.kbignum.BigNum
8 | import kotlinx.serialization.KSerializer
9 | import kotlinx.serialization.Serializer
10 | import kotlinx.serialization.descriptors.PrimitiveKind
11 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
12 | import kotlinx.serialization.encoding.Decoder
13 | import kotlinx.serialization.encoding.Encoder
14 |
15 | @Serializer(forClass = BigNum::class)
16 | object BigNumSerializer : KSerializer {
17 | override val descriptor = PrimitiveSerialDescriptor(
18 | serialName = "dev.icerock.moko.network.bignum.BigNumSerializer",
19 | kind = PrimitiveKind.STRING
20 | )
21 |
22 | override fun serialize(encoder: Encoder, value: BigNum) {
23 | encoder.encodeString(value.toString())
24 | }
25 |
26 | override fun deserialize(decoder: Decoder): BigNum {
27 | val string = decoder.decodeString()
28 | return BigNum(string)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/network-engine/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("dev.icerock.moko.gradle.detekt")
8 | id("dev.icerock.moko.gradle.publication")
9 | id("dev.icerock.moko.gradle.stub.javadoc")
10 | id("dev.icerock.moko.gradle.tests")
11 | }
12 |
13 | android {
14 | namespace = "dev.icerock.moko.network.engine"
15 | }
16 |
17 | kotlin {
18 | jvm()
19 |
20 | sourceSets {
21 | val commonMain by getting
22 |
23 | val commonJvmAndroid = create("commonJvmAndroid") {
24 | dependsOn(commonMain)
25 | dependencies {
26 | api(libs.ktorClientOkHttp)
27 | }
28 | }
29 |
30 | val androidMain by getting {
31 | dependsOn(commonJvmAndroid)
32 | }
33 |
34 | val jvmMain by getting {
35 | dependsOn(commonJvmAndroid)
36 | }
37 | }
38 | }
39 |
40 | dependencies {
41 | commonMainImplementation(libs.coroutines)
42 | commonMainApi(projects.network)
43 | iosMainApi(libs.ktorClientIos)
44 | }
45 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/sample/ios-app/src/TestViewController.swift:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import UIKit
6 | import MultiPlatformLibrary
7 |
8 | class TestViewController: UIViewController {
9 |
10 | @IBOutlet private var restText: UITextView!
11 | @IBOutlet private var webSocketText: UITextView!
12 |
13 | private var viewModel: TestViewModel!
14 |
15 | override func viewDidLoad() {
16 | super.viewDidLoad()
17 |
18 | viewModel = TestViewModel()
19 | viewModel.exceptionHandler.bind(viewController: self)
20 |
21 | viewModel.petInfo.addObserver { [weak self] info in
22 | self?.restText.text = info as String?
23 | }
24 |
25 | viewModel.websocketInfo.addObserver { [weak self] info in
26 | self?.webSocketText.text = info as String?
27 | }
28 | }
29 |
30 | @IBAction func onRefreshPressed() {
31 | viewModel.onRefreshPetPressed()
32 | }
33 |
34 | @IBAction func onRefreshWebsocketPressed() {
35 | viewModel.onRefreshWebsocketPressed()
36 | }
37 |
38 | deinit {
39 | viewModel.onCleared()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/DynamicUserAgent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.plugins
6 |
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.plugins.HttpClientPlugin
9 | import io.ktor.client.request.HttpRequestPipeline
10 | import io.ktor.client.request.header
11 | import io.ktor.http.HttpHeaders
12 | import io.ktor.util.AttributeKey
13 |
14 | class DynamicUserAgent(
15 | val agentProvider: () -> String?
16 | ) {
17 | class Config(var agentProvider: () -> String? = { null })
18 |
19 | companion object Feature : HttpClientPlugin {
20 | override val key: AttributeKey = AttributeKey("DynamicUserAgent")
21 |
22 | override fun prepare(block: Config.() -> Unit): DynamicUserAgent =
23 | DynamicUserAgent(Config().apply(block).agentProvider)
24 |
25 | override fun install(plugin: DynamicUserAgent, scope: HttpClient) {
26 | scope.requestPipeline.intercept(HttpRequestPipeline.State) {
27 | plugin.agentProvider()?.let { context.header(HttpHeaders.UserAgent, it) }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptionfactory/HttpExceptionFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptionfactory
6 |
7 | import dev.icerock.moko.network.exceptions.ResponseException
8 | import io.ktor.client.request.HttpRequest
9 | import io.ktor.client.statement.HttpResponse
10 |
11 | class HttpExceptionFactory(
12 | private val defaultParser: HttpExceptionParser,
13 | private val customParsers: Map
14 | ) : ExceptionFactory {
15 |
16 | override fun createException(
17 | request: HttpRequest,
18 | response: HttpResponse,
19 | responseBody: String?
20 | ): ResponseException {
21 | val parser = customParsers[response.status.value] ?: defaultParser
22 |
23 | val exception = parser.parseException(request, response, responseBody)
24 |
25 | return exception ?: ResponseException(request, response, responseBody.orEmpty())
26 | }
27 |
28 | fun interface HttpExceptionParser {
29 | fun parseException(
30 | request: HttpRequest,
31 | response: HttpResponse,
32 | responseBody: String?
33 | ): ResponseException?
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/nullable/NullableSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.nullable
6 |
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.builtins.nullable
9 | import kotlinx.serialization.descriptors.SerialDescriptor
10 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
11 | import kotlinx.serialization.encoding.Decoder
12 | import kotlinx.serialization.encoding.Encoder
13 |
14 | class NullableSerializer(
15 | tSerializer: KSerializer
16 | ) : KSerializer> {
17 | private val nullableTypeSerializer = tSerializer.nullable
18 |
19 | override val descriptor: SerialDescriptor = buildClassSerialDescriptor(
20 | serialName = "dev.icerock.moko.network.nullable.Nullable",
21 | nullableTypeSerializer.descriptor
22 | ) { }
23 |
24 | override fun deserialize(decoder: Decoder): Nullable {
25 | val value = nullableTypeSerializer.deserialize(decoder)
26 | return Nullable(value)
27 | }
28 |
29 | override fun serialize(encoder: Encoder, value: Nullable) {
30 | nullableTypeSerializer.serialize(encoder, value.value)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class.mustache:
--------------------------------------------------------------------------------
1 | import kotlinx.serialization.SerialName
2 | {{#hasEnums}}{{#isEnumFallbackNull}}import dev.icerock.moko.network.safeable.*{{/isEnumFallbackNull}}{{/hasEnums}}
3 |
4 | /**
5 | * {{{description}}}
6 | {{#vars}}
7 | * @param {{name}} {{{description}}}
8 | {{/vars}}
9 | */
10 | @Serializable
11 | {{>classes_modifiers}}data class {{classname}} (
12 | {{#allVars}}
13 | {{#required}}{{#optional}}{{>data_class_opt_var}}{{/optional}}{{^optional}}{{>data_class_req_var}}{{/optional}}{{/required}}{{^required}}{{#isNullable}}{{>data_class_non_req_null_var}}{{/isNullable}}{{^isNullable}}{{>data_class_non_req_non_null_var}}{{/isNullable}}{{/required}}{{^-last}},{{/-last}}
14 | {{/allVars}}
15 |
16 | ) {
17 | {{#hasEnums}}{{#vars}}{{#isEnum}}
18 | /**
19 | * {{{description}}}
20 | * Values: {{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
21 | */
22 | @Serializable
23 | enum class {{nameInCamelCase}} {
24 | {{#allowableValues}}{{#enumVars}}
25 | @SerialName({{^isString}}"{{/isString}}{{{value}}}{{^isString}}"{{/isString}})
26 | {{name}}{{^-last}},{{/-last}}{{#-last}};{{/-last}}
27 | {{/enumVars}}{{/allowableValues}}
28 | }
29 | {{/isEnum}}{{/vars}}{{/hasEnums}}
30 | }
31 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_allof.mustache:
--------------------------------------------------------------------------------
1 | import kotlinx.serialization.SerialName
2 | import kotlinx.serialization.json.Json
3 | import kotlinx.serialization.json.JsonElement
4 | import kotlinx.serialization.json.JsonObject
5 | import kotlinx.serialization.json.jsonObject
6 | import dev.icerock.moko.network.schemas.ComposedSchemaSerializer
7 |
8 | @Serializable(with = {{classname}}Serializer::class)
9 | {{>classes_modifiers}}data class {{classname}} (
10 | {{#allVars}}
11 | val {{{name}}}: {{{datatype}}}{{^-last}},{{/-last}}
12 | {{/allVars}}
13 | )
14 |
15 | {{>classes_modifiers}}object {{classname}}Serializer : ComposedSchemaSerializer<{{classname}}>("{{classname}}Serializer") {
16 |
17 | override fun decodeJson(json: Json, element: JsonElement): {{classname}} {
18 | return {{classname}}(
19 | {{#allVars}}
20 | {{{name}}} = json.decodeFromJsonElement({{{datatype}}}.serializer(), element){{^-last}},{{/-last}}
21 | {{/allVars}}
22 | )
23 | }
24 |
25 | override fun encodeJson(json: Json, value: {{classname}}): List {
26 | return listOf(
27 | {{#allVars}}
28 | json.encodeToJsonElement({{{datatype}}}.serializer(), value.{{{name}}}).jsonObject{{^-last}},{{/-last}}
29 | {{/allVars}}
30 | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/createHttpClient.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.exceptionfactory.HttpExceptionFactory
6 | import dev.icerock.moko.network.exceptionfactory.parser.ErrorExceptionParser
7 | import dev.icerock.moko.network.exceptionfactory.parser.ValidationExceptionParser
8 | import dev.icerock.moko.network.plugins.ExceptionPlugin
9 | import io.ktor.client.HttpClient
10 | import io.ktor.client.engine.mock.MockEngine
11 | import io.ktor.client.engine.mock.MockRequestHandler
12 | import io.ktor.http.HttpStatusCode
13 | import kotlinx.serialization.json.Json
14 |
15 | fun createMockClient(
16 | json: Json = Json {
17 | ignoreUnknownKeys = true
18 | },
19 | handler: MockRequestHandler
20 | ): HttpClient {
21 | return HttpClient(MockEngine) {
22 | engine {
23 | addHandler(handler)
24 | }
25 |
26 | install(ExceptionPlugin) {
27 | exceptionFactory = HttpExceptionFactory(
28 | defaultParser = ErrorExceptionParser(json),
29 | customParsers = mapOf(
30 | HttpStatusCode.UnprocessableEntity.value to ValidationExceptionParser(json)
31 | )
32 | )
33 | }
34 |
35 | expectSuccess = false
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/anyOf.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /pets:
7 | patch:
8 | requestBody:
9 | content:
10 | application/json:
11 | schema:
12 | anyOf:
13 | - $ref: '#/components/schemas/PetByAge'
14 | - $ref: '#/components/schemas/PetByType'
15 | responses:
16 | '200':
17 | description: Updated
18 | content:
19 | application/json:
20 | schema:
21 | anyOf:
22 | - $ref: '#/components/schemas/PetByAge'
23 | - $ref: '#/components/schemas/PetByType'
24 | components:
25 | schemas:
26 | PetByAge:
27 | type: object
28 | properties:
29 | age:
30 | type: integer
31 | nickname:
32 | type: string
33 | required:
34 | - age
35 |
36 | PetByType:
37 | type: object
38 | properties:
39 | pet_type:
40 | type: string
41 | enum: [Cat, Dog]
42 | hunts:
43 | type: boolean
44 | required:
45 | - pet_type
46 | TimeLineList:
47 | type: object
48 | properties:
49 | results:
50 | type: array
51 | items:
52 | anyOf:
53 | - $ref: '#/components/schemas/PetByType'
54 | - $ref: '#/components/schemas/PetByAge'
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/AllOfTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import io.ktor.client.engine.mock.MockRequestHandler
6 | import io.ktor.client.engine.mock.respondOk
7 | import kotlinx.coroutines.runBlocking
8 | import kotlinx.serialization.json.Json
9 | import openapi.allof.apis.DefaultApi
10 | import openapi.allof.models.DogAllOf
11 | import openapi.allof.models.DogComposed
12 | import openapi.allof.models.Pet
13 | import kotlin.test.Test
14 | import kotlin.test.assertEquals
15 |
16 | class AllOfTest {
17 | @Test
18 | fun `allOf - both items`() {
19 | val allOfApi = createAllOfApi {
20 | respondOk("""{"pet_type":"Dog","bark":false}""")
21 | }
22 |
23 | val result = runBlocking { allOfApi.petsPatch() }
24 |
25 | assertEquals(
26 | expected = DogComposed(
27 | item0 = Pet(petType = "Dog"),
28 | item1 = DogAllOf(bark = false)
29 | ),
30 | actual = result
31 | )
32 | }
33 |
34 | private fun createAllOfApi(mock: MockRequestHandler): DefaultApi {
35 | val json = Json.Default
36 | val httpClient = createMockClient(json, mock)
37 | return DefaultApi(
38 | basePath = "https://localhost",
39 | httpClient = httpClient,
40 | json = json
41 | )
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/network-errors/src/commonMain/resources/MR/base/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | No network connection
4 | Authorization error
5 | The requested data was not found
6 | Access denied
7 | Internal server error (%d)
8 | Invalid server data received
9 | The SSL certificate error. Secure connection failed
10 | The SSL certificate error. Server certificate has bad date
11 | The SSL certificate error. Server certificate untrusted
12 | The SSL certificate error. Server certificate has unknown root
13 | The SSL certificate error. Server certificate not yet valid
14 | The SSL certificate error. Client certificate rejected
15 | The SSL certificate error. Client certificate required
16 | The SSL certificate error. Cannot load from network
17 |
--------------------------------------------------------------------------------
/network-errors/src/commonMain/resources/MR/ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Ошибка сетевого соединения
4 | Ошибка авторизации
5 | Запрашиваемые данные не найдены
6 | Доступ запрещен
7 | Внутренняя ошибка сервера (%d)
8 | Получены некорректные данные сервера
9 | Ошибка SSL-сертификата. Сбой безопасного соединения
10 | Ошибка SSL-сертификата. Сертификат сервера имеет неверную дату
11 | Ошибка SSL-сертификата. Сертификат сервера ненадежный
12 | Ошибка SSL-сертификата. Сертификат сервера имеет неизвестный корень
13 | Ошибка SSL-сертификата. Сертификат сервера еще не действителен
14 | Ошибка SSL-сертификата. Клиентский сертификат отклонен
15 | Ошибка SSL-сертификата. Требуется сертификат клиента
16 | Ошибка SSL-сертификата. Не удается загрузить из сети
17 |
18 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/PetApiTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.generated.apis.PetApi
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.engine.mock.MockEngine
8 | import io.ktor.client.engine.mock.respondOk
9 | import kotlinx.coroutines.runBlocking
10 | import kotlinx.serialization.json.Json
11 | import tests.utils.readResourceText
12 | import kotlin.test.BeforeTest
13 | import kotlin.test.Test
14 | import kotlin.test.assertEquals
15 |
16 | class PetApiTest {
17 | private lateinit var httpClient: HttpClient
18 | private lateinit var json: Json
19 | private lateinit var petApi: PetApi
20 |
21 | @BeforeTest
22 | fun setup() {
23 | json = Json.Default
24 |
25 | httpClient = HttpClient(MockEngine) {
26 | engine {
27 | addHandler { request ->
28 | respondOk(content = readResourceText("PetstoreSearchResponse.json"))
29 | }
30 | }
31 | }
32 |
33 | petApi = PetApi(
34 | httpClient = httpClient,
35 | json = json,
36 | basePath = "https://localhost"
37 | )
38 | }
39 |
40 | @Test
41 | fun `search test`() {
42 | val result = runBlocking {
43 | petApi.findPetsByStatus(listOf("available"))
44 | }
45 |
46 | assertEquals(expected = 217, actual = result.size)
47 | }
48 | }
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/NetworkConnectionError.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import platform.Foundation.NSError
8 | import platform.Foundation.NSURLErrorCannotConnectToHost
9 | import platform.Foundation.NSURLErrorCannotFindHost
10 | import platform.Foundation.NSURLErrorCannotLoadFromNetwork
11 | import platform.Foundation.NSURLErrorDNSLookupFailed
12 | import platform.Foundation.NSURLErrorDataNotAllowed
13 | import platform.Foundation.NSURLErrorDomain
14 | import platform.Foundation.NSURLErrorNetworkConnectionLost
15 | import platform.Foundation.NSURLErrorNotConnectedToInternet
16 | import platform.Foundation.NSURLErrorResourceUnavailable
17 | import platform.Foundation.NSURLErrorTimedOut
18 |
19 | actual fun Throwable.isNetworkConnectionError(): Boolean {
20 | val nsError: NSError? = ThrowableToNSErrorMapper(this)
21 |
22 | return when {
23 | nsError?.domain == NSURLErrorDomain && nsError?.code in listOf(
24 | NSURLErrorTimedOut,
25 | NSURLErrorCannotFindHost,
26 | NSURLErrorCannotConnectToHost,
27 | NSURLErrorNetworkConnectionLost,
28 | NSURLErrorDNSLookupFailed,
29 | NSURLErrorResourceUnavailable,
30 | NSURLErrorNotConnectedToInternet,
31 | NSURLErrorDataNotAllowed,
32 | NSURLErrorCannotLoadFromNetwork,
33 | ) -> true
34 |
35 | else -> false
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/tasks/GenerateTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.tasks
6 |
7 | import dev.icerock.moko.network.KtorCodegen
8 | import dev.icerock.moko.network.SpecInfo
9 | import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
10 | import java.io.File
11 |
12 | open class GenerateTask : GenerateTask() {
13 | init {
14 | group = "moko-network"
15 | }
16 |
17 | fun configure(specInfo: SpecInfo, generatedDir: String) {
18 | inputSpec.set(specInfo.inputSpec?.path)
19 | packageName.set(specInfo.packageName)
20 |
21 | val excludedTags = specInfo.filterTags.joinToString(",")
22 | val props = mapOf(
23 | KtorCodegen.ADDITIONAL_OPTIONS_KEY_IS_INTERNAL to "${specInfo.isInternal}",
24 | KtorCodegen.ADDITIONAL_OPTIONS_KEY_IS_OPEN to "${specInfo.isOpen}",
25 | KtorCodegen.ADDITIONAL_OPTIONS_KEY_EXCLUDED_TAGS to excludedTags,
26 | KtorCodegen.ADDITIONAL_OPTIONS_KEY_ENUM_FALLBACK_NULL to "${specInfo.enumFallbackNull}"
27 | )
28 | additionalProperties.set(props)
29 |
30 | generatorName.set("kotlin-ktor-client")
31 | outputDir.set(generatedDir)
32 |
33 | specInfo.configureTask?.invoke(this)
34 |
35 | doFirst {
36 | // clean directory before generate new code
37 | File(outputDir.get()).deleteRecursively()
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Create release
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version:
7 | description: 'Version'
8 | default: '0.1.0'
9 | required: true
10 |
11 | jobs:
12 | publish:
13 | name: Publish library at mavenCentral
14 | runs-on: macOS-11
15 | env:
16 | OSSRH_USER: ${{ secrets.OSSRH_USER }}
17 | OSSRH_KEY: ${{ secrets.OSSRH_KEY }}
18 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEYID }}
19 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
20 | SIGNING_KEY: ${{ secrets.GPG_KEY_CONTENTS }}
21 |
22 | steps:
23 | - uses: actions/checkout@v1
24 | - name: Set up JDK 11
25 | uses: actions/setup-java@v1
26 | with:
27 | java-version: 11
28 | - name: Publish plugin
29 | run: ./gradlew -p network-generator publishPlugins -Pgradle.publish.key=${{ secrets.GRADLE_PLUGIN_PORTAL_KEY }} -Pgradle.publish.secret=${{ secrets.GRADLE_PLUGIN_PORTAL_SECRET }}
30 | - name: Publish library
31 | run: ./gradlew publish
32 | release:
33 | name: Create release
34 | needs: publish
35 | runs-on: ubuntu-latest
36 | steps:
37 | - name: Create Release
38 | id: create_release
39 | uses: actions/create-release@v1
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | with:
43 | commitish: ${{ github.ref }}
44 | tag_name: release/${{ github.event.inputs.version }}
45 | release_name: ${{ github.event.inputs.version }}
46 | body: "Will be filled later"
47 | draft: true
48 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/MapResponseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import io.ktor.client.engine.mock.MockRequestHandler
6 | import io.ktor.client.engine.mock.respondOk
7 | import kotlinx.coroutines.runBlocking
8 | import kotlinx.serialization.json.Json
9 | import openapi.mapResponse.apis.DefaultApi
10 | import openapi.mapResponse.models.Dog
11 | import kotlin.test.Test
12 | import kotlin.test.assertEquals
13 |
14 | class MapResponseTest {
15 | @Test
16 | fun `map in response`() {
17 | val anyOfApi = createMapResponseApi {
18 | respondOk(
19 | """
20 | {
21 | "first": [
22 | {
23 | "bark": false,
24 | "breed": "test"
25 | }
26 | ]
27 | }
28 | """.trimIndent()
29 | )
30 | }
31 |
32 | val result = runBlocking {
33 | anyOfApi.dynamicGet()
34 | }
35 |
36 | assertEquals(
37 | expected = mapOf(
38 | "first" to listOf(
39 | Dog(bark = false, breed = "test")
40 | )
41 | ),
42 | actual = result
43 | )
44 | }
45 |
46 | private fun createMapResponseApi(mock: MockRequestHandler): DefaultApi {
47 | val json = Json.Default
48 | val httpClient = createMockClient(json, mock)
49 | return DefaultApi(
50 | basePath = "https://localhost",
51 | httpClient = httpClient,
52 | json = json
53 | )
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/sample/websocket-echo-server/src/main/kotlin/com/icerockdev/server/Application.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.server
6 |
7 | import io.ktor.application.Application
8 | import io.ktor.application.call
9 | import io.ktor.application.install
10 | import io.ktor.http.ContentType
11 | import io.ktor.http.cio.websocket.Frame
12 | import io.ktor.http.cio.websocket.pingPeriod
13 | import io.ktor.http.cio.websocket.readText
14 | import io.ktor.http.cio.websocket.timeout
15 | import io.ktor.response.respondText
16 | import io.ktor.routing.get
17 | import io.ktor.routing.routing
18 | import io.ktor.websocket.WebSockets
19 | import io.ktor.websocket.webSocket
20 | import java.time.Duration
21 |
22 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args)
23 |
24 | fun Application.module() {
25 | install(WebSockets) {
26 | pingPeriod = Duration.ofSeconds(15)
27 | timeout = Duration.ofSeconds(15)
28 | maxFrameSize = Long.MAX_VALUE
29 | masking = false
30 | }
31 |
32 | routing {
33 | get("/") {
34 | call.respondText("HELLO WORLD!", contentType = ContentType.Text.Plain)
35 | }
36 |
37 | webSocket("/myws/echo") {
38 | send(Frame.Text("Hi from server"))
39 | for (i in 1..20) {
40 | val frame = incoming.receive()
41 | if (frame is Frame.Text) {
42 | send(Frame.Text("Client said: " + frame.readText()))
43 | }
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/data_class_anyof.mustache:
--------------------------------------------------------------------------------
1 | import kotlinx.serialization.SerialName
2 | import kotlinx.serialization.json.Json
3 | import kotlinx.serialization.json.JsonElement
4 | import kotlinx.serialization.json.JsonObject
5 | import kotlinx.serialization.json.jsonObject
6 | import dev.icerock.moko.network.schemas.ComposedSchemaSerializer
7 | import dev.icerock.moko.network.exceptions.DataNotFitAnyOfSchema
8 |
9 | @Serializable(with = {{classname}}Serializer::class)
10 | {{>classes_modifiers}}data class {{classname}} (
11 | {{#allVars}}
12 | val {{{name}}}: {{{datatype}}}?{{^-last}},{{/-last}}
13 | {{/allVars}}
14 | )
15 |
16 | {{>classes_modifiers}}object {{classname}}Serializer : ComposedSchemaSerializer<{{classname}}>("{{classname}}Serializer") {
17 |
18 | override fun decodeJson(json: Json, element: JsonElement): {{classname}} {
19 | {{#allVars}}
20 | val {{{name}}} = runCatching { json.decodeFromJsonElement({{{datatype}}}.serializer(), element) }
21 | {{/allVars}}
22 |
23 | ensureAnyItemIsSuccess(element, listOf({{#allVars}}{{{name}}}{{^-last}},{{/-last}}{{/allVars}}))
24 |
25 | return {{classname}}(
26 | {{#allVars}}
27 | {{{name}}} = {{{name}}}.getOrNull(){{^-last}},{{/-last}}
28 | {{/allVars}}
29 | )
30 | }
31 |
32 | override fun encodeJson(json: Json, value: {{classname}}): List {
33 | return listOfNotNull(
34 | {{#allVars}}
35 | value.{{{name}}}?.let { json.encodeToJsonElement({{{datatype}}}.serializer(), it) }{{^-last}},{{/-last}}
36 | {{/allVars}}
37 | ).map { it.jsonObject }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/TokenPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.plugins
6 |
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.plugins.HttpClientPlugin
9 | import io.ktor.client.request.HttpRequestPipeline
10 | import io.ktor.client.request.header
11 | import io.ktor.util.AttributeKey
12 |
13 | class TokenPlugin private constructor(
14 | private val tokenHeaderName: String,
15 | private val tokenProvider: TokenProvider
16 | ) {
17 |
18 | class Config {
19 | var tokenHeaderName: String? = null
20 | var tokenProvider: TokenProvider? = null
21 | fun build() = TokenPlugin(
22 | tokenHeaderName ?: throw IllegalArgumentException("HeaderName should be contain"),
23 | tokenProvider ?: throw IllegalArgumentException("TokenProvider should be contain")
24 | )
25 | }
26 |
27 | companion object Plugin : HttpClientPlugin {
28 | override val key = AttributeKey("TokenPlugin")
29 |
30 | override fun prepare(block: Config.() -> Unit) = Config().apply(block).build()
31 |
32 | override fun install(plugin: TokenPlugin, scope: HttpClient) {
33 | scope.requestPipeline.intercept(HttpRequestPipeline.State) {
34 | plugin.tokenProvider.getToken()?.apply {
35 | context.headers.remove(plugin.tokenHeaderName)
36 | context.header(plugin.tokenHeaderName, this)
37 | }
38 | }
39 | }
40 | }
41 |
42 | fun interface TokenProvider {
43 | fun getToken(): String?
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/GMTDateExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.ktor.util.date.GMTDate
8 | import platform.Foundation.NSDate
9 | import platform.Foundation.NSDateFormatter
10 | import platform.Foundation.NSLocale
11 | import platform.Foundation.currentLocale
12 | import platform.Foundation.timeIntervalSince1970
13 |
14 | private const val MILLISECONDS_IN_SECONDS = 1000
15 |
16 | @Suppress("TooGenericExceptionCaught")
17 | actual fun String.toDate(format: String): GMTDate {
18 | val formatter = NSDateFormatter()
19 | val locale = NSLocale.currentLocale()
20 | formatter.setDateFormat(format)
21 | formatter.setLocale(locale)
22 | return try {
23 | val date: NSDate = formatter.dateFromString(this)!!
24 | val timestamp: Long = (date.timeIntervalSince1970 * MILLISECONDS_IN_SECONDS).toLong()
25 | GMTDate(timestamp)
26 | } catch (npe: NullPointerException) {
27 | throw IllegalArgumentException("Parsing error: the date format is incorrect", npe)
28 | }
29 | }
30 |
31 | actual fun GMTDate.toString(format: String): String {
32 | val formatter = NSDateFormatter()
33 | val locale: NSLocale = NSLocale.currentLocale()
34 | formatter.setDateFormat(format)
35 | formatter.setLocale(locale)
36 | val timeInSeconds: Double = this.timestamp.toDouble() / MILLISECONDS_IN_SECONDS
37 | val nowSeconds: Double =
38 | NSDate().timeIntervalSince1970 - NSDate().timeIntervalSinceReferenceDate
39 | val timestamp: Double = timeInSeconds - nowSeconds
40 | val date = NSDate(timestamp)
41 | return formatter.stringFromDate(date)
42 | }
43 |
--------------------------------------------------------------------------------
/network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("dev.icerock.moko.gradle.multiplatform.mobile")
7 | id("org.jetbrains.kotlin.plugin.serialization")
8 | id("dev.icerock.moko.gradle.detekt")
9 | id("dev.icerock.moko.gradle.publication")
10 | id("dev.icerock.moko.gradle.stub.javadoc")
11 | id("dev.icerock.moko.gradle.tests")
12 | }
13 |
14 | android {
15 | namespace = "dev.icerock.moko.network"
16 | }
17 |
18 | kotlin {
19 | jvm()
20 |
21 | sourceSets {
22 | val commonMain by getting
23 |
24 | val commonJvmAndroid = create("commonJvmAndroid") {
25 | dependsOn(commonMain)
26 | }
27 |
28 | val androidMain by getting {
29 | dependsOn(commonJvmAndroid)
30 | }
31 |
32 | val jvmMain by getting {
33 | dependsOn(commonJvmAndroid)
34 | }
35 |
36 | val jvmTest by getting {
37 | dependencies {
38 | implementation(libs.kotlinTestJUnit)
39 | }
40 | }
41 | }
42 | }
43 |
44 | dependencies {
45 | commonMainImplementation(libs.coroutines)
46 | commonMainApi(libs.kotlinSerialization)
47 | commonMainApi(libs.ktorClient)
48 |
49 | androidMainImplementation(libs.appCompat)
50 |
51 | commonTestImplementation(libs.ktorClientMock)
52 | commonTestImplementation(libs.kotlinTest)
53 | commonTestImplementation(libs.kotlinTestAnnotations)
54 |
55 | androidTestImplementation(libs.kotlinTestJUnit)
56 | }
57 |
58 | tasks.named("publishToMavenLocal") {
59 | val pluginPublish = gradle.includedBuild("network-generator")
60 | .task(":publishToMavenLocal")
61 | dependsOn(pluginPublish)
62 | }
63 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/LanguagePlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.plugins
6 |
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.plugins.HttpClientPlugin
9 | import io.ktor.client.request.HttpRequestPipeline
10 | import io.ktor.client.request.header
11 | import io.ktor.util.AttributeKey
12 |
13 | class LanguagePlugin private constructor(
14 | private val languageHeaderName: String,
15 | private val languageProvider: LanguagePlugin.LanguageCodeProvider
16 | ) {
17 | class Config {
18 | var languageHeaderName: String? = null
19 | var languageCodeProvider: LanguageCodeProvider? = null
20 | fun build() = LanguagePlugin(
21 | languageHeaderName ?: throw IllegalArgumentException("HeaderName should be contain"),
22 | languageCodeProvider
23 | ?: throw IllegalArgumentException("LanguageCodeProvider should be contain")
24 | )
25 | }
26 |
27 | companion object Plugin : HttpClientPlugin {
28 | override val key = AttributeKey("LanguagePlugin")
29 |
30 | override fun prepare(block: Config.() -> Unit) = Config().apply(block).build()
31 |
32 | override fun install(plugin: LanguagePlugin, scope: HttpClient) {
33 | scope.requestPipeline.intercept(HttpRequestPipeline.State) {
34 | plugin.languageProvider.getLanguageCode()?.apply {
35 | context.header(plugin.languageHeaderName, this)
36 | }
37 | }
38 | }
39 | }
40 |
41 | interface LanguageCodeProvider {
42 | fun getLanguageCode(): String?
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/allOf.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | /pets:
7 | patch:
8 | requestBody:
9 | content:
10 | application/json:
11 | schema:
12 | oneOf:
13 | - $ref: '#/components/schemas/Cat'
14 | - $ref: '#/components/schemas/Dog'
15 | discriminator:
16 | propertyName: pet_type
17 | responses:
18 | '200':
19 | description: Updated
20 | content:
21 | application/json:
22 | schema:
23 | $ref: '#/components/schemas/Dog'
24 | components:
25 | schemas:
26 | Pet:
27 | type: object
28 | required:
29 | - pet_type
30 | properties:
31 | pet_type:
32 | type: string
33 | discriminator:
34 | propertyName: pet_type
35 | Dog: # "Dog" is a value for the pet_type property (the discriminator value)
36 | allOf: # Combines the main `Pet` schema with `Dog`-specific properties
37 | - $ref: '#/components/schemas/Pet'
38 | - type: object
39 | # all other properties specific to a `Dog`
40 | properties:
41 | bark:
42 | type: boolean
43 | breed:
44 | type: string
45 | enum: [Dingo, Husky, Retriever, Shepherd]
46 | Cat: # "Cat" is a value for the pet_type property (the discriminator value)
47 | allOf: # Combines the main `Pet` schema with `Cat`-specific properties
48 | - $ref: '#/components/schemas/Pet'
49 | - type: object
50 | # all other properties specific to a `Cat`
51 | properties:
52 | hunts:
53 | type: boolean
54 | age:
55 | type: integer
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/MultiPlatformNetworkGeneratorDeprecatedPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import org.gradle.api.Plugin
8 | import org.gradle.api.Project
9 | import org.gradle.api.tasks.Delete
10 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
11 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
12 | import org.openapitools.generator.gradle.plugin.OpenApiGeneratorPlugin
13 |
14 | class MultiPlatformNetworkGeneratorDeprecatedPlugin : Plugin {
15 | private val openApiGenerator = OpenApiGeneratorPlugin()
16 |
17 | override fun apply(target: Project) {
18 | openApiGenerator.apply(target)
19 |
20 | val generatedDir = "${target.buildDir}/generate-resources/main"
21 |
22 | target.afterEvaluate { it.setupProject(generatedDir) }
23 | }
24 |
25 | private fun Project.setupProject(generatedDir: String) {
26 | extensions.findByType(KotlinMultiplatformExtension::class.java)?.run {
27 | val sourceSet = sourceSets.getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
28 | val sources = "$generatedDir/src/main/kotlin"
29 | sourceSet.kotlin.srcDir(sources)
30 | }
31 |
32 | val removeGeneratedCodeTask =
33 | tasks.create("removeGeneratedOpenApiCode", Delete::class.java) {
34 | delete(file(generatedDir))
35 | }
36 |
37 | tasks.findByName("openApiGenerate")?.let {
38 | it.dependsOn(removeGeneratedCodeTask)
39 |
40 | tasks.findByName("preBuild")?.dependsOn(it)
41 | tasks.findByName("compileKotlinIosX64")?.dependsOn(it)
42 | tasks.findByName("compileKotlinIosArm64")?.dependsOn(it)
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/network-errors/src/commonMain/kotlin/dev/icerock/moko/network/errors/NetworkErrorsTexts.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.errors
6 |
7 | import dev.icerock.moko.resources.StringResource
8 |
9 | data class NetworkErrorsTexts(
10 | val networkConnectionErrorText: StringResource = MR.strings.networkConnectionErrorText,
11 | val serializationErrorText: StringResource = MR.strings.serializationErrorText,
12 | val httpNetworkErrorsTexts: HttpNetworkErrorsTexts = HttpNetworkErrorsTexts(),
13 | val sslNetworkErrorsTexts: SSLNetworkErrorsTexts = SSLNetworkErrorsTexts()
14 | )
15 |
16 | data class HttpNetworkErrorsTexts(
17 | val unauthorizedErrorText: StringResource = MR.strings.unauthorizedErrorText,
18 | val notFoundErrorText: StringResource = MR.strings.notFoundErrorText,
19 | val accessDeniedErrorText: StringResource = MR.strings.accessDeniedErrorText,
20 | val internalServerErrorText: StringResource = MR.strings.internalServerErrorText
21 | )
22 |
23 | data class SSLNetworkErrorsTexts(
24 | val secureConnectionFailed: StringResource = MR.strings.secureConnectionFailedText,
25 | val serverCertificateHasBadDate: StringResource = MR.strings.serverCertificateHasBadDateText,
26 | val serverCertificateUntrusted: StringResource = MR.strings.serverCertificateUntrustedText,
27 | val serverCertificateHasUnknownRoot: StringResource = MR.strings.serverCertificateHasUnknownRootText,
28 | val serverCertificateNotYetValid: StringResource = MR.strings.serverCertificateNotYetValidText,
29 | val clientCertificateRejected: StringResource = MR.strings.clientCertificateRejectedText,
30 | val clientCertificateRequired: StringResource = MR.strings.clientCertificateRequiredText,
31 | val cannotLoadFromNetwork: StringResource = MR.strings.cannotLoadFromNetworkText
32 | )
33 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/HttpExt.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import dev.icerock.moko.network.exceptions.ResponseException
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.call.ReceivePipelineException
10 | import io.ktor.client.call.body
11 | import io.ktor.client.request.request
12 | import io.ktor.client.request.setBody
13 | import io.ktor.client.request.url
14 | import io.ktor.client.utils.EmptyContent
15 | import io.ktor.http.ContentType
16 | import io.ktor.http.Headers
17 | import io.ktor.http.HttpMethod
18 | import io.ktor.http.contentType
19 |
20 | operator fun Headers.plus(other: Headers): Headers = when {
21 | this.isEmpty() -> other
22 | other.isEmpty() -> this
23 | else -> Headers.build {
24 | appendAll(this@plus)
25 | appendAll(other)
26 | }
27 | }
28 |
29 | suspend inline fun HttpClient.createRequest(
30 | path: String,
31 | methodType: HttpMethod = HttpMethod.Get,
32 | body: Any = EmptyContent,
33 | contentType: ContentType? = null
34 | ): Value {
35 | @Suppress("SwallowedException")
36 | try {
37 | return request {
38 | method = methodType
39 | url(path)
40 | if (contentType != null) contentType(contentType)
41 | setBody(body)
42 | }.body()
43 | } catch (e: ReceivePipelineException) {
44 | if (e.cause is ResponseException) {
45 | throw e.cause
46 | } else {
47 | throw e
48 | }
49 | }
50 | }
51 |
52 | suspend inline fun HttpClient.createJsonRequest(
53 | path: String,
54 | methodType: HttpMethod = HttpMethod.Get,
55 | body: Any = EmptyContent
56 | ): Value =
57 | createRequest(path, methodType, body, ContentType.Application.Json)
58 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/safeable/SafeableSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.safeable
6 |
7 | import kotlinx.serialization.KSerializer
8 | import kotlinx.serialization.SerializationException
9 | import kotlinx.serialization.descriptors.SerialDescriptor
10 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor
11 | import kotlinx.serialization.encoding.Decoder
12 | import kotlinx.serialization.encoding.Encoder
13 | import kotlin.native.concurrent.ThreadLocal
14 |
15 | class SafeableSerializer(
16 | tSerializer: KSerializer
17 | ) : KSerializer> {
18 | private val typeSerializer = tSerializer
19 |
20 | override val descriptor: SerialDescriptor = buildClassSerialDescriptor(
21 | serialName = "dev.icerock.moko.network.safeable.Safeable",
22 | typeSerializer.descriptor
23 | ) { }
24 |
25 | override fun deserialize(decoder: Decoder): Safeable {
26 | return try {
27 | val result = typeSerializer.deserialize(decoder)
28 | Safeable(result)
29 | } catch (cause: SerializationException) {
30 | val handler = deserializeExceptionHandler ?: return Safeable(null)
31 |
32 | if (handler(cause)) {
33 | Safeable(null)
34 | } else {
35 | throw cause
36 | }
37 | }
38 | }
39 |
40 | override fun serialize(encoder: Encoder, value: Safeable) {
41 | if (value.value == null) {
42 | throw SerializationException("Can't encode Safeable with null value")
43 | }
44 |
45 | typeSerializer.serialize(encoder, value.value)
46 | }
47 |
48 | @ThreadLocal
49 | companion object {
50 | var deserializeExceptionHandler: ((SerializationException) -> Boolean)? = null
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Do’s and Don’ts
2 |
3 | * **Search tickets before you file a new one.** Add to tickets if you have new information about the issue.
4 | * **Keep tickets short but sweet.** Make sure you include all the context needed to solve the issue. Don't overdo it. Great tickets allow us to focus on solving problems instead of discussing them.
5 | * **Take care of your ticket.** When you spend time to report a ticket with care we'll enjoy fixing it for you.
6 | * **Use [GitHub-flavored Markdown](https://help.github.com/articles/markdown-basics/).** Especially put code blocks and console outputs in backticks (```` ``` ````). That increases the readability. Bonus points for applying the appropriate syntax highlighting.
7 |
8 | ## Bug Reports
9 |
10 | In short, since you are most likely a developer, provide a ticket that you _yourself_ would _like_ to receive.
11 |
12 | First check if you are using the latest library version and Kotlin version before filing a ticket.
13 |
14 | Please include steps to reproduce and _all_ other relevant information, including any other relevant dependency and version information.
15 |
16 | ## Feature Requests
17 |
18 | Please try to be precise about the proposed outcome of the feature and how it
19 | would related to existing features.
20 |
21 |
22 | ## Pull Requests
23 |
24 | We **love** pull requests!
25 |
26 | All contributions _will_ be licensed under the Apache 2 license.
27 |
28 | Code/comments should adhere to the following rules:
29 |
30 | * Names should be descriptive and concise.
31 | * Use four spaces and no tabs.
32 | * Remember that source code usually gets written once and read often: ensure
33 | the reader doesn't have to make guesses. Make sure that the purpose and inner
34 | logic are either obvious to a reasonably skilled professional, or add a
35 | comment that explains it.
36 | * Please add a detailed description.
37 |
38 | If you consistently contribute improvements and/or bug fixes, we're happy to make you a maintainer.
--------------------------------------------------------------------------------
/sample/form-data-binary-server/src/main/kotlin/com/icerockdev/server/Application.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.server
6 |
7 | import io.ktor.http.content.PartData
8 | import io.ktor.http.content.forEachPart
9 | import io.ktor.http.content.streamProvider
10 | import io.ktor.server.application.Application
11 | import io.ktor.server.application.call
12 | import io.ktor.server.request.receiveMultipart
13 | import io.ktor.server.response.respondText
14 | import io.ktor.server.routing.post
15 | import io.ktor.server.routing.routing
16 | import java.io.File
17 | import kotlin.collections.mutableMapOf
18 | import kotlin.collections.set
19 |
20 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args)
21 |
22 | fun Application.module() {
23 | routing {
24 | post("/v1/auth/signup") {
25 | val map = mutableMapOf()
26 | val multipartData = call.receiveMultipart()
27 |
28 | multipartData.forEachPart { part ->
29 | if (part is PartData.FormItem) {
30 | map[part.name] = part.value
31 | }
32 |
33 | if (part is PartData.FileItem) {
34 | writePartFile(part)
35 | }
36 |
37 | part.dispose()
38 | }
39 |
40 | call.respondText(successResponse)
41 | }
42 | }
43 | }
44 |
45 | private fun writePartFile(part: PartData.FileItem) {
46 | val fileName = part.originalFileName as String
47 | val fileBytes = part.streamProvider().readBytes()
48 | val dir = File("uploads")
49 | dir.mkdir()
50 | File(dir, "$fileName.jpg").writeBytes(fileBytes)
51 | }
52 |
53 | val successResponse = """
54 | {
55 | "status": 200,
56 | "message": "avatar uploaded",
57 | "timestamp": 123.0,
58 | "success": true
59 | }
60 | """.trimIndent()
--------------------------------------------------------------------------------
/network/src/iosMain/kotlin/dev/icerock/moko/network/isSSLException.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | @file:Suppress("Filename")
6 |
7 | package dev.icerock.moko.network
8 |
9 | import platform.Foundation.NSError
10 | import platform.Foundation.NSURLErrorCannotLoadFromNetwork
11 | import platform.Foundation.NSURLErrorClientCertificateRequired
12 | import platform.Foundation.NSURLErrorDomain
13 | import platform.Foundation.NSURLErrorSecureConnectionFailed
14 | import platform.Foundation.NSURLErrorServerCertificateHasBadDate
15 | import platform.Foundation.NSURLErrorServerCertificateHasUnknownRoot
16 | import platform.Foundation.NSURLErrorServerCertificateNotYetValid
17 | import platform.Foundation.NSURLErrorServerCertificateUntrusted
18 |
19 | private val sslKeys = mapOf(
20 | NSURLErrorSecureConnectionFailed to SSLExceptionType.SecureConnectionFailed,
21 | NSURLErrorServerCertificateHasBadDate to SSLExceptionType.ServerCertificateHasBadDate,
22 | NSURLErrorServerCertificateUntrusted to SSLExceptionType.ServerCertificateUntrusted,
23 | NSURLErrorServerCertificateHasUnknownRoot to SSLExceptionType.ServerCertificateHasUnknownRoot,
24 | NSURLErrorServerCertificateNotYetValid to SSLExceptionType.ServerCertificateNotYetValid,
25 | NSURLErrorClientCertificateRequired to SSLExceptionType.ClientCertificateRequired,
26 | NSURLErrorCannotLoadFromNetwork to SSLExceptionType.CannotLoadFromNetwork
27 | )
28 |
29 | actual fun Throwable.isSSLException(): Boolean {
30 | val nsError: NSError = ThrowableToNSErrorMapper(this) ?: return false
31 |
32 | return nsError.domain == NSURLErrorDomain && sslKeys.keys.contains(nsError.code)
33 | }
34 |
35 | @Suppress("ReturnCount")
36 | actual fun Throwable.getSSLExceptionType(): SSLExceptionType? {
37 | val nsError: NSError = ThrowableToNSErrorMapper(this) ?: return null
38 | if (nsError.domain != NSURLErrorDomain) return null
39 |
40 | return sslKeys[nsError.code]
41 | }
42 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/ExceptionPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.plugins
6 |
7 | import dev.icerock.moko.network.exceptionfactory.ExceptionFactory
8 | import io.ktor.client.HttpClient
9 | import io.ktor.client.plugins.HttpClientPlugin
10 | import io.ktor.client.plugins.HttpSend
11 | import io.ktor.client.plugins.plugin
12 | import io.ktor.client.statement.bodyAsChannel
13 | import io.ktor.http.isSuccess
14 | import io.ktor.util.AttributeKey
15 | import io.ktor.utils.io.charsets.Charset
16 | import io.ktor.utils.io.core.readText
17 |
18 | class ExceptionPlugin(private val exceptionFactory: ExceptionFactory) {
19 |
20 | class Config {
21 | var exceptionFactory: ExceptionFactory? = null
22 | fun build() = ExceptionPlugin(
23 | exceptionFactory
24 | ?: throw IllegalArgumentException("Exception factory should be contain")
25 | )
26 | }
27 |
28 | companion object Plugin : HttpClientPlugin {
29 |
30 | override val key = AttributeKey("ExceptionPlugin")
31 |
32 | override fun prepare(block: Config.() -> Unit) = Config().apply(block).build()
33 |
34 | override fun install(plugin: ExceptionPlugin, scope: HttpClient) {
35 | scope.plugin(HttpSend).intercept { request ->
36 | val call = execute(request)
37 | if (!call.response.status.isSuccess()) {
38 | val packet = call.response.bodyAsChannel().readRemaining()
39 | val responseString = packet.readText(charset = Charset.forName("UTF-8"))
40 | throw plugin.exceptionFactory.createException(
41 | request = call.request,
42 | response = call.response,
43 | responseBody = responseString
44 | )
45 | }
46 | call
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | moko-network
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(BUNDLE_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 0.1.0
21 | CFBundleVersion
22 | 1
23 | LSApplicationCategoryType
24 | public.app-category.developer-tools
25 | LSRequiresIPhoneOS
26 |
27 | NSAppTransportSecurity
28 |
29 | NSAllowsArbitraryLoads
30 |
31 |
32 | NSMainStoryboardFile
33 | Main
34 | UILaunchStoryboardName
35 | LaunchScreen
36 | UIMainStoryboardFile
37 | Main
38 | UIRequiredDeviceCapabilities
39 |
40 | armv7
41 |
42 | UIRequiresFullScreen
43 |
44 | UIStatusBarHidden
45 |
46 | UIStatusBarHidden~ipad
47 |
48 | UIStatusBarStyle
49 | UIStatusBarStyleLightContent
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationLandscapeRight
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationPortraitUpsideDown
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/AnyTypeTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.nullable.asNullable
6 | import io.ktor.client.engine.mock.MockRequestHandler
7 | import io.ktor.client.engine.mock.respondOk
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.serialization.json.Json
10 | import kotlinx.serialization.json.JsonNull
11 | import kotlinx.serialization.json.JsonPrimitive
12 | import kotlinx.serialization.json.buildJsonObject
13 | import kotlinx.serialization.json.put
14 | import openapi.anyType.apis.DefaultApi
15 | import openapi.anyType.models.Resp
16 | import kotlin.test.Test
17 | import kotlin.test.assertEquals
18 |
19 | class AnyTypeTest {
20 | @Test
21 | fun `any type in response`() {
22 | val api = createApi {
23 | respondOk(
24 | """
25 | {
26 | "anyProp": "test",
27 | "anyList": [
28 | "test2",
29 | 3,
30 | {
31 | "name": "none"
32 | },
33 | null
34 | ]
35 | }
36 | """.trimIndent()
37 | )
38 | }
39 |
40 | val result = runBlocking {
41 | api.dynamicGet()
42 | }
43 |
44 | assertEquals(
45 | expected = Resp(
46 | anyProp = JsonPrimitive("test").asNullable(),
47 | anyList = listOf(
48 | JsonPrimitive("test2"),
49 | JsonPrimitive(3),
50 | buildJsonObject {
51 | put("name", "none")
52 | },
53 | JsonNull
54 | )
55 | ),
56 | actual = result
57 | )
58 | }
59 |
60 | private fun createApi(mock: MockRequestHandler): DefaultApi {
61 | val json = Json.Default
62 | val httpClient = createMockClient(json, mock)
63 | return DefaultApi(
64 | basePath = "https://localhost",
65 | httpClient = httpClient,
66 | json = json
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/SchemaContext.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.swagger.v3.oas.models.Operation
8 | import io.swagger.v3.oas.models.PathItem
9 | import io.swagger.v3.oas.models.media.MediaType
10 | import io.swagger.v3.oas.models.parameters.Parameter
11 | import io.swagger.v3.oas.models.parameters.RequestBody
12 | import io.swagger.v3.oas.models.responses.ApiResponse
13 |
14 | sealed class SchemaContext {
15 | data class OperationResponse(
16 | val pathName: String,
17 | val pathItem: PathItem,
18 | val method: PathItem.HttpMethod,
19 | val operation: Operation,
20 | val responseName: String,
21 | val response: ApiResponse,
22 | val contentName: String,
23 | val mediaType: MediaType
24 | ) : SchemaContext()
25 |
26 | data class Response(
27 | val responseName: String,
28 | val response: ApiResponse,
29 | val contentName: String,
30 | val mediaType: MediaType
31 | ) : SchemaContext()
32 |
33 | data class OperationRequest(
34 | val pathName: String,
35 | val pathItem: PathItem,
36 | val method: PathItem.HttpMethod,
37 | val operation: Operation,
38 | val requestBody: RequestBody,
39 | val contentName: String,
40 | val mediaType: MediaType
41 | ) : SchemaContext()
42 |
43 | data class Request(
44 | val requestName: String,
45 | val requestBody: RequestBody,
46 | val contentName: String,
47 | val mediaType: MediaType
48 | ) : SchemaContext()
49 |
50 | data class ParameterComponent(
51 | val parameterName: String,
52 | val parameter: Parameter
53 | ) : SchemaContext()
54 |
55 | data class SchemaComponent(
56 | val schemaName: String
57 | ) : SchemaContext()
58 |
59 | data class PropertyComponent(
60 | val schemaName: String?,
61 | val propertyName: String
62 | ) : SchemaContext()
63 |
64 | data class Child(
65 | val parent: SchemaContext,
66 | val child: SchemaContext
67 | ) : SchemaContext()
68 | }
69 |
--------------------------------------------------------------------------------
/sample/mpp-library/MultiPlatformLibrary.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = 'MultiPlatformLibrary'
3 | spec.version = '0.1.0'
4 | spec.homepage = 'Link to a Kotlin/Native module homepage'
5 | spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
6 | spec.authors = 'IceRock Development'
7 | spec.license = ''
8 | spec.summary = 'Shared code between iOS and Android'
9 |
10 | spec.vendored_frameworks = "build/cocoapods/framework/#{spec.name}.framework"
11 | spec.libraries = "c++"
12 | spec.module_name = "#{spec.name}_umbrella"
13 |
14 | spec.ios.deployment_target = '11.0'
15 | spec.osx.deployment_target = '10.6'
16 |
17 | spec.pod_target_xcconfig = {
18 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*ebug]' => 'debug',
19 | 'KOTLIN_FRAMEWORK_BUILD_TYPE[config=*elease]' => 'release',
20 | 'CURENT_SDK[sdk=iphoneos*]' => 'iphoneos',
21 | 'CURENT_SDK[sdk=iphonesimulator*]' => 'iphonesimulator',
22 | 'CURENT_SDK[sdk=macosx*]' => 'macos'
23 | }
24 |
25 | spec.script_phases = [
26 | {
27 | :name => 'Compile Kotlin/Native',
28 | :execution_position => :before_compile,
29 | :shell_path => '/bin/sh',
30 | :script => <<-SCRIPT
31 | if [ "$KOTLIN_FRAMEWORK_BUILD_TYPE" == "debug" ]; then
32 | CONFIG="Debug"
33 | else
34 | CONFIG="Release"
35 | fi
36 |
37 | if [ "$CURENT_SDK" == "iphoneos" ]; then
38 | TARGET="Ios"
39 | ARCH="Arm64"
40 | elif [ "$CURENT_SDK" == "macos" ]; then
41 | TARGET="Macos"
42 | if [ "$NATIVE_ARCH" == "arm64" ]; then
43 | ARCH="Arm64"
44 | else
45 | ARCH="X64"
46 | fi
47 | else
48 | if [ "$NATIVE_ARCH" == "arm64" ]; then
49 | TARGET="IosSimulator"
50 | ARCH="Arm64"
51 | else
52 | TARGET="Ios"
53 | ARCH="X64"
54 | fi
55 | fi
56 |
57 | MPP_PROJECT_ROOT="$SRCROOT/../../mpp-library"
58 | GRADLE_TASK="syncMultiPlatformLibrary${CONFIG}Framework${TARGET}${ARCH}"
59 |
60 | "$MPP_PROJECT_ROOT/../gradlew" -p "$MPP_PROJECT_ROOT" "$GRADLE_TASK"
61 | SCRIPT
62 | }
63 | ]
64 | end
65 |
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/TokenFeatureTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.plugins.TokenPlugin
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.engine.mock.MockEngine
8 | import io.ktor.client.engine.mock.MockRequestHandler
9 | import io.ktor.client.engine.mock.respondBadRequest
10 | import io.ktor.client.engine.mock.respondOk
11 | import io.ktor.client.request.get
12 | import io.ktor.http.HttpStatusCode
13 | import kotlinx.coroutines.runBlocking
14 | import kotlin.test.Test
15 | import kotlin.test.assertEquals
16 |
17 | class TokenFeatureTest {
18 | @Test
19 | fun `token added when exist`() {
20 | val client = createMockClient(
21 | tokenProvider = { "mytoken" }
22 | ) { request ->
23 | if (request.headers[AUTH_HEADER_NAME] == "mytoken") respondOk()
24 | else respondBadRequest()
25 | }
26 |
27 | val result = runBlocking {
28 | client.get("localhost")
29 | }
30 |
31 | assertEquals(expected = HttpStatusCode.OK, actual = result.status)
32 | }
33 |
34 | @Test
35 | fun `token not added when not exist`() {
36 | val client = createMockClient(
37 | tokenProvider = { null }
38 | ) { request ->
39 | if (request.headers.contains(AUTH_HEADER_NAME).not()) respondOk()
40 | else respondBadRequest()
41 | }
42 |
43 | val result = runBlocking {
44 | client.get("localhost")
45 | }
46 |
47 | assertEquals(expected = HttpStatusCode.OK, actual = result.status)
48 | }
49 |
50 | private fun createMockClient(
51 | tokenProvider: TokenPlugin.TokenProvider,
52 | handler: MockRequestHandler
53 | ): HttpClient {
54 | return HttpClient(MockEngine) {
55 | engine {
56 | addHandler(handler)
57 | }
58 |
59 | install(TokenPlugin) {
60 | this.tokenHeaderName = AUTH_HEADER_NAME
61 | this.tokenProvider = tokenProvider
62 | }
63 | }
64 | }
65 |
66 | private companion object {
67 | const val AUTH_HEADER_NAME = "Auth"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptionfactory/parser/ErrorExceptionParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptionfactory.parser
6 |
7 | import dev.icerock.moko.network.exceptionfactory.HttpExceptionFactory
8 | import dev.icerock.moko.network.exceptions.ErrorException
9 | import dev.icerock.moko.network.exceptions.ResponseException
10 | import io.ktor.client.request.HttpRequest
11 | import io.ktor.client.statement.HttpResponse
12 | import kotlinx.serialization.json.Json
13 | import kotlinx.serialization.json.contentOrNull
14 | import kotlinx.serialization.json.intOrNull
15 | import kotlinx.serialization.json.jsonObject
16 | import kotlinx.serialization.json.jsonPrimitive
17 |
18 | class ErrorExceptionParser(private val json: Json) : HttpExceptionFactory.HttpExceptionParser {
19 |
20 | override fun parseException(
21 | request: HttpRequest,
22 | response: HttpResponse,
23 | responseBody: String?
24 | ): ResponseException? {
25 | @Suppress("TooGenericExceptionCaught", "SwallowedException")
26 | try {
27 | val body = responseBody.orEmpty()
28 | val jsonRoot = json.parseToJsonElement(body)
29 | var jsonObject = jsonRoot.jsonObject
30 | if (jsonObject.containsKey(JSON_ERROR_KEY)) {
31 | jsonObject = jsonObject.getValue(JSON_ERROR_KEY).jsonObject
32 | }
33 |
34 | var message: String? = null
35 | var code = response.status.value
36 |
37 | if (jsonObject.containsKey(JSON_MESSAGE_KEY)) {
38 | message = jsonObject[JSON_MESSAGE_KEY]?.jsonPrimitive?.contentOrNull
39 | }
40 | if (jsonObject.containsKey(JSON_CODE_KEY)) {
41 | val newCode = jsonObject[JSON_CODE_KEY]?.jsonPrimitive?.intOrNull
42 | if (newCode != null) {
43 | code = newCode
44 | }
45 | }
46 | return ErrorException(request, response, code, message)
47 | } catch (e: Exception) {
48 | return null
49 | }
50 | }
51 |
52 | companion object {
53 | private const val JSON_MESSAGE_KEY = "message"
54 | private const val JSON_CODE_KEY = "code"
55 | private const val JSON_ERROR_KEY = "error"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
17 |
18 |
24 |
25 |
31 |
32 |
36 |
37 |
43 |
44 |
50 |
51 |
52 |
56 |
57 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/NullableTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.nullable.Nullable
6 | import kotlinx.serialization.Serializable
7 | import kotlinx.serialization.json.Json
8 | import kotlin.test.Test
9 | import kotlin.test.assertEquals
10 |
11 | class NullableTest {
12 | @Test
13 | fun `nullable with default encode`() {
14 | val json = Json.Default
15 | val data = TestData()
16 |
17 | val result = json.encodeToString(TestData.serializer(), data)
18 | assertEquals(expected = "{}", actual = result)
19 | }
20 |
21 | @Test
22 | fun `nullable with null encode`() {
23 | val json = Json.Default
24 | val data = TestData(data = Nullable(value = null))
25 |
26 | val result = json.encodeToString(TestData.serializer(), data)
27 | assertEquals(expected = """{"data":null}""", actual = result)
28 | }
29 |
30 | @Test
31 | fun `nullable with value encode`() {
32 | val json = Json.Default
33 | val data = TestData(data = Nullable(value = "test"))
34 |
35 | val result = json.encodeToString(TestData.serializer(), data)
36 | assertEquals(expected = """{"data":"test"}""", actual = result)
37 | }
38 |
39 |
40 | @Test
41 | fun `nullable with default decode`() {
42 | val json = Json.Default
43 | val input = "{}"
44 |
45 | val result = json.decodeFromString(TestData.serializer(), input)
46 | assertEquals(expected = TestData(), actual = result)
47 | }
48 |
49 | @Test
50 | fun `nullable with null decode`() {
51 | val json = Json.Default
52 | val input = """{"data":null}"""
53 |
54 | val result = json.decodeFromString(TestData.serializer(), input)
55 | assertEquals(
56 | expected = TestData(data = null),
57 | actual = result
58 | ) // for now we cant check key exists
59 | }
60 |
61 | @Test
62 | fun `nullable with value decode`() {
63 | val json = Json.Default
64 | val input = """{"data":"test"}"""
65 |
66 | val result = json.decodeFromString(TestData.serializer(), input)
67 | assertEquals(expected = TestData(data = Nullable(value = "test")), actual = result)
68 | }
69 |
70 | @Serializable
71 | data class TestData(
72 | val data: Nullable? = null
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/network-generator/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("com.gradle.plugin-publish") version ("0.15.0")
7 | id("java-gradle-plugin")
8 | }
9 |
10 | buildscript {
11 | repositories {
12 | mavenCentral()
13 | google()
14 | gradlePluginPortal()
15 | }
16 | dependencies {
17 | classpath(libs.kotlinGradlePlugin)
18 | classpath(libs.mokoGradlePlugin)
19 | classpath(libs.kotlinSerializationGradlePlugin)
20 | }
21 | }
22 |
23 | apply(plugin = "org.jetbrains.kotlin.jvm")
24 | apply(plugin = "dev.icerock.moko.gradle.detekt")
25 | apply(plugin = "dev.icerock.moko.gradle.publication")
26 | apply(plugin = "dev.icerock.moko.gradle.publication.nexus")
27 |
28 | group = "dev.icerock.moko"
29 | version = libs.versions.mokoNetworkVersion.get()
30 |
31 | dependencies {
32 | implementation(gradleKotlinDsl())
33 | compileOnly(libs.kotlinGradlePlugin)
34 | implementation(libs.guava)
35 | implementation(libs.openApiGenerator)
36 | }
37 |
38 | java {
39 | sourceCompatibility = JavaVersion.VERSION_1_8
40 | targetCompatibility = JavaVersion.VERSION_1_8
41 | withJavadocJar()
42 | withSourcesJar()
43 | }
44 |
45 | configure {
46 | publications.register("mavenJava", MavenPublication::class) {
47 | from(components["java"])
48 | }
49 | }
50 |
51 | tasks.withType().configureEach {
52 | kotlinOptions.jvmTarget = "1.8"
53 | }
54 |
55 | gradlePlugin {
56 | plugins {
57 | create("multiplatform-network-generator") {
58 | id = "dev.icerock.mobile.multiplatform-network-generator"
59 | implementationClass = "dev.icerock.moko.network.MultiPlatformNetworkGeneratorPlugin"
60 | }
61 | }
62 | }
63 |
64 | pluginBundle {
65 | website = "https://github.com/icerockdev/moko-network"
66 | vcsUrl = "https://github.com/icerockdev/moko-network"
67 | description = "Plugin to provide network components for iOS & Android"
68 | tags = listOf("moko-network", "moko", "kotlin", "kotlin-multiplatform")
69 |
70 | plugins {
71 | getByName("multiplatform-network-generator") {
72 | displayName = "MOKO network generator plugin"
73 | }
74 | }
75 |
76 | mavenCoordinates {
77 | groupId = project.group as String
78 | artifactId = project.name
79 | version = project.version as String
80 | }
81 | }
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/LanguageFeatureTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.plugins.LanguagePlugin
6 | import io.ktor.client.HttpClient
7 | import io.ktor.client.engine.mock.MockEngine
8 | import io.ktor.client.engine.mock.MockRequestHandler
9 | import io.ktor.client.engine.mock.respondBadRequest
10 | import io.ktor.client.engine.mock.respondOk
11 | import io.ktor.client.request.get
12 | import io.ktor.client.statement.HttpResponse
13 | import io.ktor.http.HttpStatusCode
14 | import kotlinx.coroutines.runBlocking
15 | import kotlin.test.Test
16 | import kotlin.test.assertEquals
17 |
18 | class LanguageFeatureTest {
19 | @Test
20 | fun `language added when exist`() {
21 | val client = createMockClient(
22 | provider = object : LanguagePlugin.LanguageCodeProvider {
23 | override fun getLanguageCode(): String = "ru"
24 | },
25 | handler = { request ->
26 | if (request.headers[LANGUAGE_HEADER_NAME] == "ru") respondOk()
27 | else respondBadRequest()
28 | }
29 | )
30 |
31 | val result = runBlocking {
32 | client.get("localhost")
33 | }
34 |
35 | assertEquals(expected = HttpStatusCode.OK, actual = result.status)
36 | }
37 |
38 | @Test
39 | fun `language not added when not exist`() {
40 | val client = createMockClient(
41 | provider = object : LanguagePlugin.LanguageCodeProvider {
42 | override fun getLanguageCode(): String? = null
43 | },
44 | handler = { request ->
45 | if (request.headers.contains(LANGUAGE_HEADER_NAME).not()) respondOk()
46 | else respondBadRequest()
47 | }
48 | )
49 |
50 | val result = runBlocking {
51 | client.get("localhost")
52 | }
53 |
54 | assertEquals(expected = HttpStatusCode.OK, actual = result.status)
55 | }
56 |
57 | private fun createMockClient(
58 | provider: LanguagePlugin.LanguageCodeProvider,
59 | handler: MockRequestHandler
60 | ): HttpClient {
61 | return HttpClient(MockEngine) {
62 | engine {
63 | addHandler(handler)
64 | }
65 |
66 | install(LanguagePlugin) {
67 | this.languageHeaderName = LANGUAGE_HEADER_NAME
68 | this.languageCodeProvider = provider
69 | }
70 | }
71 | }
72 |
73 | private companion object {
74 | const val LANGUAGE_HEADER_NAME = "Lang"
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/enumFallbackNull.yaml:
--------------------------------------------------------------------------------
1 | openapi: 3.0.0
2 | info:
3 | title: API
4 | version: v1
5 | paths:
6 | '/carcolors':
7 | get:
8 | operationId: car_colors
9 | responses:
10 | '200':
11 | description: ''
12 | content:
13 | application/json:
14 | schema:
15 | anyOf:
16 | - $ref: '#/components/schemas/CarColor'
17 | - $ref: '#/components/schemas/CarColorDefault'
18 | - $ref: '#/components/schemas/CarColorRequired'
19 | - $ref: '#/components/schemas/CarColorNullable'
20 | '/carcolorsList':
21 | get:
22 | operationId: car_colors_list
23 | responses:
24 | '200':
25 | description: ''
26 | content:
27 | application/json:
28 | schema:
29 | $ref: '#/components/schemas/CarColorList'
30 | components:
31 | schemas:
32 | CarColor:
33 | properties:
34 | color:
35 | type: string
36 | enum:
37 | - black
38 | - white
39 | - red
40 | - green
41 | - blue
42 | CarColorDefault:
43 | properties:
44 | color:
45 | type: string
46 | default: red
47 | enum:
48 | - black
49 | - white
50 | - red
51 | - green
52 | - blue
53 | CarColorList:
54 | properties:
55 | color:
56 | type: array
57 | items:
58 | type: string
59 | enum:
60 | - black
61 | - white
62 | - red
63 | - green
64 | - blue
65 | CarColorListNullable:
66 | properties:
67 | color:
68 | type: array
69 | nullable: true
70 | items:
71 | type: string
72 | enum:
73 | - black
74 | - white
75 | - red
76 | - green
77 | - blue
78 | CarColorRequired:
79 | properties:
80 | color:
81 | type: string
82 | enum:
83 | - black
84 | - white
85 | - red
86 | - green
87 | - blue
88 | required:
89 | - color
90 | CarColorNullable:
91 | properties:
92 | color:
93 | type: string
94 | nullable: true
95 | enum:
96 | - black
97 | - white
98 | - red
99 | - green
100 | - blue
101 |
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/AllOfTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.schemas.ComposedSchemaSerializer
6 | import kotlinx.serialization.SerialName
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.json.Json
9 | import kotlinx.serialization.json.JsonElement
10 | import kotlinx.serialization.json.JsonObject
11 | import kotlinx.serialization.json.jsonObject
12 | import kotlin.test.Test
13 | import kotlin.test.assertEquals
14 |
15 | class AllOfTest {
16 | private val json = Json.Default
17 |
18 | @Test
19 | fun `allOf decode`() {
20 | val input = """{"pet_type":"Dog","bark":false,"breed":"Dingo"}"""
21 |
22 | val output = json.decodeFromString(Dog.serializer(), input)
23 |
24 | assertEquals(
25 | expected = Dog(
26 | pet = Pet(petType = "Dog"),
27 | inlineDog = InlineDog(bark = false, breed = "Dingo")
28 | ),
29 | actual = output
30 | )
31 | }
32 |
33 | @Test
34 | fun `allOf encode`() {
35 | val input = Dog(
36 | pet = Pet(petType = "Dog"),
37 | inlineDog = InlineDog(bark = false, breed = "Dingo")
38 | )
39 |
40 | val output = json.encodeToString(Dog.serializer(), input)
41 |
42 | assertEquals(
43 | expected = """{"pet_type":"Dog","bark":false,"breed":"Dingo"}""",
44 | actual = output
45 | )
46 | }
47 | }
48 |
49 |
50 | @Serializable
51 | private data class Pet(
52 | @SerialName("pet_type")
53 | val petType: String
54 | )
55 |
56 | // allOf
57 | @Serializable(with = DogSerializer::class)
58 | private data class Dog(
59 | val pet: Pet,
60 | val inlineDog: InlineDog
61 | )
62 |
63 | @Serializable
64 | private data class InlineDog(
65 | val bark: Boolean? = null,
66 | val breed: String? = null
67 | )
68 |
69 | private object DogSerializer : ComposedSchemaSerializer("DogSerializer") {
70 |
71 | override fun decodeJson(json: Json, element: JsonElement): Dog {
72 | val pet = json.decodeFromJsonElement(Pet.serializer(), element)
73 | val inlineDog = json.decodeFromJsonElement(InlineDog.serializer(), element)
74 |
75 | return Dog(
76 | pet = pet,
77 | inlineDog = inlineDog
78 | )
79 | }
80 |
81 | override fun encodeJson(json: Json, value: Dog): List {
82 | val pet = json.encodeToJsonElement(Pet.serializer(), value.pet).jsonObject
83 | val inlineDog = json.encodeToJsonElement(InlineDog.serializer(), value.inlineDog).jsonObject
84 |
85 | return listOf(pet, inlineDog)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/MultiPlatformNetworkGeneratorPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import dev.icerock.moko.network.tasks.GenerateTask
8 | import org.gradle.api.Plugin
9 | import org.gradle.api.Project
10 | import org.gradle.api.Task
11 | import org.gradle.api.tasks.TaskProvider
12 | import org.gradle.kotlin.dsl.getByType
13 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
14 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
15 | import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
16 | import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinNativeCompile
17 |
18 | class MultiPlatformNetworkGeneratorPlugin : Plugin {
19 |
20 | override fun apply(project: Project) {
21 | val mokoNetworkExtension = project.extensions.create("mokoNetwork", SpecConfig::class.java)
22 |
23 | project.afterEvaluate {
24 | it.setupProject(mokoNetworkExtension)
25 | }
26 | }
27 |
28 | private fun Project.setupProject(mokoNetworkExtension: SpecConfig) {
29 | val multiplatformExtension: KotlinMultiplatformExtension = project.extensions.getByType()
30 |
31 | if (mokoNetworkExtension.specs.isEmpty()) return
32 |
33 | val openApiGenerateTask = tasks.create("openApiGenerate") {
34 | it.group = "moko-network"
35 | }
36 |
37 | mokoNetworkExtension.specs.forEach { spec ->
38 | registerSpecGenerationTask(
39 | spec = spec,
40 | openApiGenerateTask = openApiGenerateTask,
41 | multiplatformExtension = multiplatformExtension
42 | )
43 | }
44 |
45 | tasks.matching { it.name == "preBuild" }
46 | .all { it.dependsOn(openApiGenerateTask) }
47 | tasks.withType(AbstractKotlinCompile::class.java)
48 | .all { it.dependsOn(openApiGenerateTask) }
49 | tasks.withType(AbstractKotlinNativeCompile::class.java)
50 | .all { it.dependsOn(openApiGenerateTask) }
51 | }
52 | }
53 |
54 | private fun Project.registerSpecGenerationTask(
55 | spec: SpecInfo,
56 | openApiGenerateTask: Task,
57 | multiplatformExtension: KotlinMultiplatformExtension,
58 | ) {
59 | val generatedDir = "$buildDir/generated/moko-network/${spec.name}"
60 | val generatedSourcesDir = "$generatedDir/src/main/kotlin"
61 | val sourceSet: KotlinSourceSet? = multiplatformExtension.sourceSets.getByName(spec.sourceSet)
62 |
63 | sourceSet?.kotlin?.srcDir(generatedSourcesDir)
64 |
65 | val generateTask: TaskProvider = tasks.register(
66 | "${spec.name}OpenApiGenerate",
67 | GenerateTask::class.java
68 | ) { it.configure(spec, generatedDir) }
69 |
70 | openApiGenerateTask.dependsOn(generateTask)
71 | }
72 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/schemas/ComposedSchemaSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.schemas
6 |
7 | import dev.icerock.moko.network.exceptions.DataNotFitAnyOfSchema
8 | import dev.icerock.moko.network.exceptions.DataNotFitOneOfSchema
9 | import kotlinx.serialization.KSerializer
10 | import kotlinx.serialization.descriptors.PrimitiveKind
11 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
12 | import kotlinx.serialization.descriptors.SerialDescriptor
13 | import kotlinx.serialization.encoding.Decoder
14 | import kotlinx.serialization.encoding.Encoder
15 | import kotlinx.serialization.json.Json
16 | import kotlinx.serialization.json.JsonDecoder
17 | import kotlinx.serialization.json.JsonElement
18 | import kotlinx.serialization.json.JsonEncoder
19 | import kotlinx.serialization.json.JsonObject
20 | import kotlinx.serialization.json.buildJsonObject
21 |
22 | abstract class ComposedSchemaSerializer(serialName: String) : KSerializer {
23 | override val descriptor: SerialDescriptor =
24 | PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING)
25 |
26 | override fun deserialize(decoder: Decoder): T {
27 | decoder as JsonDecoder
28 |
29 | val jsonElement = decoder.decodeSerializableValue(JsonElement.serializer())
30 |
31 | val json = Json(from = decoder.json) {
32 | ignoreUnknownKeys = true
33 | }
34 |
35 | return decodeJson(json, jsonElement)
36 | }
37 |
38 | override fun serialize(encoder: Encoder, value: T) {
39 | encoder as JsonEncoder
40 |
41 | val jsonObjects = encodeJson(encoder.json, value)
42 |
43 | val outputObject = buildJsonObject {
44 | jsonObjects.forEach { jsonObject ->
45 | jsonObject.entries.forEach { (key, value) ->
46 | put(key, value)
47 | }
48 | }
49 | }
50 |
51 | encoder.encodeJsonElement(outputObject)
52 | }
53 |
54 | abstract fun decodeJson(json: Json, element: JsonElement): T
55 |
56 | abstract fun encodeJson(json: Json, value: T): List
57 |
58 | protected fun ensureAnyItemIsSuccess(element: JsonElement, results: List>) {
59 | val isAllFailed = results.all { it.isFailure }
60 |
61 | if (isAllFailed.not()) return
62 |
63 | throw DataNotFitAnyOfSchema(
64 | data = element,
65 | causes = results.mapNotNull { it.exceptionOrNull() }
66 | )
67 | }
68 |
69 | protected fun ensureOnlyOneItemIsSuccess(element: JsonElement, results: List>) {
70 | val failedCount = results.count { it.isFailure }
71 | if (failedCount == results.size - 1) return
72 |
73 | throw DataNotFitOneOfSchema(
74 | data = element,
75 | results = results
76 | )
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/sample/ios-app/src/Resources/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/java/com/icerockdev/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package com.icerockdev.app
6 |
7 | import android.graphics.Bitmap
8 | import android.os.Bundle
9 | import android.widget.Button
10 | import android.widget.TextView
11 | import android.widget.Toast
12 | import androidx.core.content.ContextCompat
13 | import androidx.core.graphics.drawable.toBitmap
14 | import androidx.lifecycle.ViewModelProvider
15 | import com.icerockdev.app.databinding.ActivityMainBinding
16 | import com.icerockdev.library.TestViewModel
17 | import dev.icerock.moko.mvvm.MvvmActivity
18 | import dev.icerock.moko.mvvm.createViewModelFactory
19 | import io.ktor.utils.io.core.Input
20 | import io.ktor.utils.io.streams.asInput
21 | import java.io.ByteArrayInputStream
22 | import java.io.ByteArrayOutputStream
23 |
24 | class MainActivity : MvvmActivity() {
25 | override val layoutId: Int = R.layout.activity_main
26 | override val viewModelClass: Class = TestViewModel::class.java
27 | override val viewModelVariableId: Int = BR.viewModel
28 |
29 | override fun viewModelFactory(): ViewModelProvider.Factory {
30 | return createViewModelFactory { TestViewModel() }
31 | }
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 |
36 | viewModel.exceptionHandler.bind(this, this)
37 |
38 | val restText: TextView = findViewById(R.id.restText)
39 | val websocketText: TextView = findViewById(R.id.websocketText)
40 | val petsRefreshButton: Button = findViewById(R.id.refreshButton)
41 | val websocketRefreshButton: Button = findViewById(R.id.websocketButton)
42 | val fakeSignupButton: Button = findViewById(R.id.fakeSignupWithAvatar)
43 |
44 | viewModel.petInfo.ld().observe(this) { data ->
45 | restText.text = data
46 | }
47 | viewModel.websocketInfo.ld().observe(this) { data ->
48 | websocketText.text = data
49 | }
50 |
51 | petsRefreshButton.setOnClickListener {
52 | viewModel.onRefreshPetPressed()
53 | }
54 |
55 | websocketRefreshButton.setOnClickListener {
56 | viewModel.onRefreshWebsocketPressed()
57 | }
58 |
59 | fakeSignupButton.setOnClickListener {
60 | viewModel.fakeSignupWithAvatar(makeFakeAvatar())
61 | }
62 |
63 | viewModel.eventsDispatcher.bind(
64 | this,
65 | object : TestViewModel.EventListener {
66 | override fun onFakeSignupResult(result: String) {
67 | Toast.makeText(this@MainActivity, result, Toast.LENGTH_SHORT).show()
68 | }
69 | }
70 | )
71 | }
72 |
73 | @Suppress("MagicNumber")
74 | private fun makeFakeAvatar(): Input {
75 | val byteArrayOutputStream = ByteArrayOutputStream()
76 | val avatar = requireNotNull(ContextCompat.getDrawable(this, R.drawable.logo)).toBitmap()
77 | avatar.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
78 | return ByteArrayInputStream(byteArrayOutputStream.toByteArray()).asInput()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/network-generator/src/main/resources/kotlin-ktor-client/api_doc.mustache:
--------------------------------------------------------------------------------
1 | # {{classname}}{{#description}}
2 | {{description}}{{/description}}
3 |
4 | All URIs are relative to *{{basePath}}*
5 |
6 | Method | HTTP request | Description
7 | ------------- | ------------- | -------------
8 | {{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}}
9 | {{/operation}}{{/operations}}
10 |
11 | {{#operations}}
12 | {{#operation}}
13 |
14 | # **{{operationId}}**
15 | > {{#returnType}}{{returnType}} {{/returnType}}{{operationId}}({{#allParams}}{{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/allParams}})
16 |
17 | {{summary}}{{#notes}}
18 |
19 | {{notes}}{{/notes}}
20 |
21 | ### Example
22 | ```kotlin
23 | // Import classes:
24 | //import {{{packageName}}}.infrastructure.*
25 | //import {{{modelPackage}}}.*
26 |
27 | {{! TODO: Auth method documentation examples}}
28 | val apiInstance = {{{classname}}}()
29 | {{#allParams}}
30 | val {{{paramName}}} : {{{dataType}}} = {{{example}}} // {{{dataType}}} | {{{description}}}
31 | {{/allParams}}
32 | try {
33 | {{#returnType}}val result : {{{returnType}}} = {{/returnType}}apiInstance.{{{operationId}}}({{#allParams}}{{{paramName}}}{{#hasMore}}, {{/hasMore}}{{/allParams}}){{#returnType}}
34 | println(result){{/returnType}}
35 | } catch (e: ClientException) {
36 | println("4xx response calling {{{classname}}}#{{{operationId}}}")
37 | e.printStackTrace()
38 | } catch (e: ServerException) {
39 | println("5xx response calling {{{classname}}}#{{{operationId}}}")
40 | e.printStackTrace()
41 | }
42 | ```
43 |
44 | ### Parameters
45 | {{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#allParams}}{{#-last}}
46 | Name | Type | Description | Notes
47 | ------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}}
48 | {{#allParams}} **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}{{#generateModelDocs}}[**{{dataType}}**]({{baseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{dataType}}**{{/generateModelDocs}}{{/isFile}}{{/isPrimitiveType}}| {{description}} |{{^required}} [optional]{{/required}}{{#defaultValue}} [default to {{defaultValue}}]{{/defaultValue}}{{#allowableValues}} [enum: {{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}]{{/allowableValues}}
49 | {{/allParams}}
50 |
51 | ### Return type
52 |
53 | {{#returnType}}{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}{{#generateModelDocs}}[**{{returnType}}**]({{returnBaseType}}.md){{/generateModelDocs}}{{^generateModelDocs}}**{{returnType}}**{{/generateModelDocs}}{{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}null (empty response body){{/returnType}}
54 |
55 | ### Authorization
56 |
57 | {{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{name}}](../README.md#{{name}}){{^-last}}, {{/-last}}{{/authMethods}}
58 |
59 | ### HTTP request headers
60 |
61 | - **Content-Type**: {{#consumes}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/consumes}}{{^consumes}}Not defined{{/consumes}}
62 | - **Accept**: {{#produces}}{{{mediaType}}}{{#hasMore}}, {{/hasMore}}{{/produces}}{{^produces}}Not defined{{/produces}}
63 |
64 | {{/operation}}
65 | {{/operations}}
66 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/plugins/RefreshTokenPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.plugins
6 |
7 | import io.ktor.client.HttpClient
8 | import io.ktor.client.plugins.HttpClientPlugin
9 | import io.ktor.client.request.HttpRequest
10 | import io.ktor.client.request.HttpRequestBuilder
11 | import io.ktor.client.request.request
12 | import io.ktor.client.request.takeFrom
13 | import io.ktor.client.statement.HttpReceivePipeline
14 | import io.ktor.client.statement.HttpResponse
15 | import io.ktor.client.statement.request
16 | import io.ktor.http.HttpStatusCode
17 | import io.ktor.util.AttributeKey
18 | import kotlinx.coroutines.sync.Mutex
19 | import kotlinx.coroutines.sync.withLock
20 |
21 | class RefreshTokenPlugin(
22 | private val updateTokenHandler: suspend () -> Boolean,
23 | private val isCredentialsActual: (HttpRequest) -> Boolean
24 | ) {
25 |
26 | class Config {
27 | var updateTokenHandler: (suspend () -> Boolean)? = null
28 | var isCredentialsActual: ((HttpRequest) -> Boolean)? = null
29 |
30 | fun build() = RefreshTokenPlugin(
31 | updateTokenHandler
32 | ?: throw IllegalArgumentException("updateTokenHandler should be passed"),
33 | isCredentialsActual
34 | ?: throw IllegalArgumentException("isCredentialsActual should be passed")
35 | )
36 | }
37 |
38 | companion object Plugin : HttpClientPlugin {
39 |
40 | private val refreshTokenHttpPluginMutex = Mutex()
41 |
42 | override val key = AttributeKey("RefreshTokenPlugin")
43 |
44 | override fun prepare(block: Config.() -> Unit) = Config().apply(block).build()
45 |
46 | override fun install(plugin: RefreshTokenPlugin, scope: HttpClient) {
47 | scope.receivePipeline.intercept(HttpReceivePipeline.After) {
48 | if (subject.status != HttpStatusCode.Unauthorized) {
49 | proceedWith(subject)
50 | return@intercept
51 | }
52 |
53 | refreshTokenHttpPluginMutex.withLock {
54 | // If token of the request isn't actual, then token has already been updated and
55 | // let's just to try repeat request
56 | if (!plugin.isCredentialsActual(subject.request)) {
57 | val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
58 | val result: HttpResponse = scope.request(requestBuilder)
59 | proceedWith(result)
60 | return@intercept
61 | }
62 |
63 | // Else if token of the request is actual (same as in the storage), then need to send
64 | // refresh request.
65 | if (plugin.updateTokenHandler.invoke()) {
66 | // If the request refresh was successful, then let's just to try repeat request
67 | val requestBuilder = HttpRequestBuilder().takeFrom(subject.request)
68 | val result: HttpResponse = scope.request(requestBuilder)
69 | proceedWith(result)
70 | } else {
71 | // If the request refresh was unsuccessful
72 | proceedWith(subject)
73 | }
74 | }
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/FormDataTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import cases.formData.apis.AuthApi
6 | import cases.formData.models.Response
7 | import cases.formData.models.SignupRequest
8 | import io.ktor.client.engine.mock.MockRequestHandler
9 | import io.ktor.client.engine.mock.respondOk
10 | import io.ktor.client.engine.mock.toByteArray
11 | import io.ktor.client.request.forms.MultiPartFormDataContent
12 | import io.ktor.http.content.OutgoingContent
13 | import io.ktor.utils.io.core.ByteReadPacket
14 | import kotlinx.coroutines.runBlocking
15 | import kotlinx.serialization.json.Json
16 | import kotlin.test.Test
17 | import kotlin.test.assertEquals
18 | import kotlin.test.assertTrue
19 |
20 | class FormDataTest {
21 | @Test
22 | fun `formData body`() {
23 | val api = createApi { request ->
24 | val body: OutgoingContent = request.body
25 | assertTrue(body is MultiPartFormDataContent)
26 |
27 | val content: String = body.toByteArray().decodeToString()
28 | val contentWithFixedBoundary: String = content
29 | .replace(body.boundary, "==boundary==")
30 | .replace("\r\n", "\n")
31 | assertEquals(
32 | expected = """--==boundary==
33 | Content-Disposition: form-data; name=signup
34 | Content-Length: 150
35 |
36 | {"firstName":"first","lastName":"last","phone":"+799","email":"a@b","password":"111","passwordRepeat":"111","countryId":1,"cityId":2,"company":"test"}
37 | --==boundary==
38 | Content-Disposition: form-data; name=avatar; filename=avatar
39 |
40 | ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB
41 | --==boundary==--
42 | """,
43 | actual = contentWithFixedBoundary
44 | )
45 |
46 | respondOk(
47 | """
48 | {
49 | "status": 200,
50 | "message": "ok",
51 | "timestamp": 1001.0,
52 | "success": true
53 | }
54 | """.trimIndent()
55 | )
56 | }
57 |
58 | val avatarBytes = ByteArray(100) { index ->
59 | if (index % 2 == 0) 'A'.toByte()
60 | else 'B'.toByte()
61 | }
62 | val result = runBlocking {
63 | api.signup(
64 | signup = SignupRequest(
65 | firstName = "first",
66 | lastName = "last",
67 | phone = "+799",
68 | email = "a@b",
69 | password = "111",
70 | passwordRepeat = "111",
71 | countryId = 1,
72 | cityId = 2,
73 | company = "test"
74 | ),
75 | avatar = ByteReadPacket(avatarBytes)
76 | )
77 | }
78 |
79 | assertEquals(
80 | expected = Response(
81 | status = 200,
82 | message = "ok",
83 | timestamp = 1001.0,
84 | success = true
85 | ),
86 | actual = result
87 | )
88 | }
89 |
90 | private fun createApi(mock: MockRequestHandler): AuthApi {
91 | val json = Json.Default
92 | val httpClient = createMockClient(json, mock)
93 | return AuthApi(
94 | basePath = "https://localhost",
95 | httpClient = httpClient,
96 | json = json
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/exceptionfactory/parser/ValidationExceptionParser.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.exceptionfactory.parser
6 |
7 | import dev.icerock.moko.network.exceptionfactory.HttpExceptionFactory
8 | import dev.icerock.moko.network.exceptions.ErrorException
9 | import dev.icerock.moko.network.exceptions.ResponseException
10 | import dev.icerock.moko.network.exceptions.ValidationException
11 | import io.ktor.client.request.HttpRequest
12 | import io.ktor.client.statement.HttpResponse
13 | import kotlinx.serialization.json.Json
14 | import kotlinx.serialization.json.JsonArray
15 | import kotlinx.serialization.json.JsonObject
16 | import kotlinx.serialization.json.int
17 | import kotlinx.serialization.json.jsonArray
18 | import kotlinx.serialization.json.jsonObject
19 | import kotlinx.serialization.json.jsonPrimitive
20 |
21 | class ValidationExceptionParser(private val json: Json) : HttpExceptionFactory.HttpExceptionParser {
22 |
23 | @Suppress("ReturnCount", "NestedBlockDepth")
24 | override fun parseException(
25 | request: HttpRequest,
26 | response: HttpResponse,
27 | responseBody: String?
28 | ): ResponseException? {
29 | @Suppress("TooGenericExceptionCaught", "SwallowedException")
30 | try {
31 | val body = responseBody.orEmpty()
32 | val jsonRoot = json.parseToJsonElement(body)
33 | if (jsonRoot is JsonObject) {
34 | val error = jsonRoot.jsonObject.getValue(JSON_ERROR_KEY).jsonObject
35 |
36 | return ErrorException(
37 | request = request,
38 | response = response,
39 | code = error.getValue(JSON_CODE_KEY).jsonPrimitive.int,
40 | description = error.getValue(JSON_MESSAGE_KEY).jsonPrimitive.content
41 | )
42 | } else if (jsonRoot is JsonArray) {
43 | val errorsJson = jsonRoot.jsonArray
44 |
45 | val errors = ArrayList(errorsJson.size)
46 |
47 | errorsJson.forEach { item ->
48 | try {
49 | val jsonObject = item.jsonObject
50 |
51 | val message: String
52 | val field: String
53 |
54 | if (jsonObject.containsKey(JSON_MESSAGE_KEY)) {
55 | message = jsonObject.getValue(JSON_MESSAGE_KEY).jsonPrimitive.content
56 | } else {
57 | return@forEach
58 | }
59 |
60 | if (jsonObject.containsKey(JSON_FIELD_KEY)) {
61 | field = jsonObject.getValue(JSON_FIELD_KEY).jsonPrimitive.content
62 | } else {
63 | return@forEach
64 | }
65 |
66 | errors.add(ValidationException.Error(field, message))
67 | } catch (e: Exception) {
68 | // ignore item
69 | }
70 | }
71 |
72 | return ValidationException(request, response, responseBody.orEmpty(), errors)
73 | } else {
74 | return null
75 | }
76 | } catch (e: Exception) {
77 | return null
78 | }
79 | }
80 |
81 | companion object {
82 | private const val JSON_MESSAGE_KEY = "message"
83 | private const val JSON_FIELD_KEY = "field"
84 | private const val JSON_ERROR_KEY = "error"
85 | private const val JSON_CODE_KEY = "code"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/AnyOfTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import io.ktor.client.engine.mock.MockRequestHandler
6 | import io.ktor.client.engine.mock.respondOk
7 | import io.ktor.http.content.TextContent
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.serialization.json.Json
10 | import openapi.anyof.apis.DefaultApi
11 | import openapi.anyof.models.PetByAge
12 | import openapi.anyof.models.PetByType
13 | import openapi.anyof.models.PetsPatchRequestBodyComposed
14 | import openapi.anyof.models.PetsPatchResponse200Composed
15 | import kotlin.test.Test
16 | import kotlin.test.assertEquals
17 | import kotlin.test.assertTrue
18 |
19 | class AnyOfTest {
20 | @Test
21 | fun `anyOf - both items`() {
22 | val anyOfApi = createAnyOfApi { request ->
23 | val body = request.body
24 | assertTrue(body is TextContent)
25 | assertEquals(
26 | expected = """{"age":4,"pet_type":"Cat","hunts":true}""",
27 | actual = body.text
28 | )
29 | respondOk(body.text)
30 | }
31 |
32 | val petByAge = PetByAge(age = 4)
33 | val petByType = PetByType(hunts = true, petType = PetByType.PetType.CAT)
34 |
35 | val result = runBlocking {
36 | anyOfApi.petsPatch(
37 | PetsPatchRequestBodyComposed(item0 = petByAge, item1 = petByType)
38 | )
39 | }
40 |
41 | assertEquals(
42 | expected = PetsPatchResponse200Composed(item0 = petByAge, item1 = petByType),
43 | actual = result
44 | )
45 | }
46 |
47 | @Test
48 | fun `anyOf - first item`() {
49 | val anyOfApi = createAnyOfApi { request ->
50 | val body = request.body
51 | assertTrue(body is TextContent)
52 | assertEquals(
53 | expected = """{"age":4}""",
54 | actual = body.text
55 | )
56 | respondOk(body.text)
57 | }
58 |
59 | val petByAge = PetByAge(age = 4)
60 |
61 | val result = runBlocking {
62 | anyOfApi.petsPatch(
63 | PetsPatchRequestBodyComposed(item0 = petByAge, item1 = null)
64 | )
65 | }
66 |
67 | assertEquals(
68 | expected = PetsPatchResponse200Composed(item0 = petByAge, item1 = null),
69 | actual = result
70 | )
71 | }
72 |
73 | @Test
74 | fun `anyOf - second item`() {
75 | val anyOfApi = createAnyOfApi { request ->
76 | val body = request.body
77 | assertTrue(body is TextContent)
78 | assertEquals(
79 | expected = """{"pet_type":"Cat","hunts":true}""",
80 | actual = body.text
81 | )
82 | respondOk(body.text)
83 | }
84 |
85 | val petByType = PetByType(hunts = true, petType = PetByType.PetType.CAT)
86 |
87 | val result = runBlocking {
88 | anyOfApi.petsPatch(
89 | PetsPatchRequestBodyComposed(item0 = null, item1 = petByType)
90 | )
91 | }
92 |
93 | assertEquals(
94 | expected = PetsPatchResponse200Composed(item0 = null, item1 = petByType),
95 | actual = result
96 | )
97 | }
98 |
99 | private fun createAnyOfApi(mock: MockRequestHandler): DefaultApi {
100 | val json = Json.Default
101 | val httpClient = createMockClient(json, mock)
102 | return DefaultApi(
103 | basePath = "https://localhost",
104 | httpClient = httpClient,
105 | json = json
106 | )
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/network/src/commonMain/kotlin/dev/icerock/moko/network/multipart/MultiPartContent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.multipart
6 |
7 | import dev.icerock.moko.network.plus
8 | import io.ktor.http.ContentType
9 | import io.ktor.http.Headers
10 | import io.ktor.http.HttpHeaders
11 | import io.ktor.http.content.OutgoingContent
12 | import io.ktor.http.headersOf
13 | import io.ktor.http.withCharset
14 | import io.ktor.util.flattenEntries
15 | import io.ktor.utils.io.ByteWriteChannel
16 | import io.ktor.utils.io.charsets.Charsets
17 | import io.ktor.utils.io.writeFully
18 | import io.ktor.utils.io.writeStringUtf8
19 | import kotlin.random.Random
20 |
21 | // based on https://github.com/ktorio/ktor-samples/blob/master/other/client-multipart/src/MultipartApp.kt
22 | data class MultiPartContent(val parts: List) : OutgoingContent.WriteChannelContent() {
23 | @Suppress("MagicNumber")
24 | private val boundary = buildString {
25 | repeat(32) {
26 | append(Random.nextInt().toString(16))
27 | }
28 | }.take(70)
29 |
30 | data class Part(
31 | val name: String,
32 | val filename: String? = null,
33 | val headers: Headers = Headers.Empty,
34 | val writer: suspend ByteWriteChannel.() -> Unit
35 | )
36 |
37 | override suspend fun writeTo(channel: ByteWriteChannel) {
38 | for (part in parts) {
39 | channel.writeStringUtf8("--$boundary\r\n")
40 | val partHeaders = Headers.build {
41 | val fileNamePart =
42 | if (part.filename != null) "; filename=\"${part.filename}\"" else ""
43 | append("Content-Disposition", "form-data; name=\"${part.name}\"$fileNamePart")
44 | appendAll(part.headers)
45 | }
46 | for ((key, value) in partHeaders.flattenEntries()) {
47 | channel.writeStringUtf8("$key: $value\r\n")
48 | }
49 | channel.writeStringUtf8("\r\n")
50 | part.writer(channel)
51 | channel.writeStringUtf8("\r\n")
52 | }
53 | channel.writeStringUtf8("--$boundary--\r\n")
54 | }
55 |
56 | override val contentType = ContentType.MultiPart.FormData
57 | .withParameter("boundary", boundary)
58 | .withCharset(Charsets.UTF_8)
59 |
60 | class Builder {
61 | private val parts = arrayListOf()
62 |
63 | fun add(part: Part) {
64 | parts += part
65 | }
66 |
67 | fun add(
68 | name: String,
69 | filename: String? = null,
70 | contentType: ContentType? = null,
71 | headers: Headers = Headers.Empty,
72 | writer: suspend ByteWriteChannel.() -> Unit
73 | ) {
74 | val contentTypeHeaders: Headers = if (contentType != null) {
75 | headersOf(
76 | HttpHeaders.ContentType,
77 | contentType.toString()
78 | )
79 | } else {
80 | headersOf()
81 | }
82 | add(Part(name, filename, headers + contentTypeHeaders, writer))
83 | }
84 |
85 | fun add(
86 | name: String,
87 | text: String,
88 | contentType: ContentType? = null,
89 | filename: String? = null
90 | ) {
91 | add(name, filename, contentType) { writeStringUtf8(text) }
92 | }
93 |
94 | fun add(
95 | name: String,
96 | data: ByteArray,
97 | contentType: ContentType? = ContentType.Application.OctetStream,
98 | filename: String? = null
99 | ) {
100 | add(name, filename, contentType) { writeFully(data) }
101 | }
102 |
103 | internal fun build(): MultiPartContent = MultiPartContent(parts.toList())
104 | }
105 |
106 | companion object {
107 | fun build(callback: Builder.() -> Unit) = Builder().apply(callback).build()
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/sample/mpp-library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | plugins {
6 | id("com.android.library")
7 | id("dev.icerock.moko.gradle.android.base")
8 | id("org.jetbrains.kotlin.multiplatform")
9 | id("org.jetbrains.kotlin.plugin.serialization")
10 | id("dev.icerock.mobile.multiplatform.targets")
11 | id("dev.icerock.mobile.multiplatform-resources")
12 | id("dev.icerock.mobile.multiplatform-network-generator")
13 | id("dev.icerock.mobile.multiplatform.ios-framework")
14 | id("dev.icerock.moko.gradle.detekt")
15 | id("dev.icerock.moko.gradle.tests")
16 | }
17 |
18 | dependencies {
19 | commonMainImplementation(libs.coroutines)
20 | commonMainImplementation(libs.ktorClient)
21 | commonMainImplementation(libs.ktorClientLogging)
22 | commonMainImplementation(libs.kotlinSerialization)
23 | commonMainImplementation(libs.ktorClientWebSocket)
24 | commonMainImplementation(libs.kbignum)
25 |
26 | commonMainApi(libs.mokoMvvmCore)
27 | commonMainApi(libs.mokoMvvmLiveData)
28 |
29 | commonMainApi(projects.network)
30 | commonMainApi(projects.networkEngine)
31 | commonMainApi(projects.networkBignum)
32 | commonMainApi(projects.networkErrors)
33 |
34 | androidMainImplementation(libs.lifecycleViewModel)
35 |
36 | commonTestImplementation(libs.ktorClientMock)
37 | commonTestImplementation(libs.kotlinTest)
38 | commonTestImplementation(libs.mokoTest)
39 | commonTestImplementation(libs.kotlinTestAnnotations)
40 |
41 | androidTestImplementation(libs.kotlinTestJUnit)
42 | }
43 |
44 | multiplatformResources {
45 | multiplatformResourcesPackage = "com.icerockdev.library"
46 | }
47 |
48 | mokoNetwork {
49 | spec("pets") {
50 | inputSpec = file("src/swagger.json")
51 | }
52 | spec("profile") {
53 | inputSpec = file("src/profile_openapi.yaml")
54 | isInternal = false
55 | isOpen = false
56 | }
57 | spec("news") {
58 | inputSpec = file("wrong file")
59 | packageName = "news"
60 | isInternal = false
61 | isOpen = true
62 | configureTask {
63 | inputSpec.set(file("src/newsApi.yaml").path)
64 | }
65 | }
66 | spec("allOf") {
67 | packageName = "openapi.allof"
68 | inputSpec = file("src/allOf.yaml")
69 | }
70 | spec("anyOf") {
71 | packageName = "openapi.anyof"
72 | inputSpec = file("src/anyOf.yaml")
73 | }
74 | spec("oneOf") {
75 | packageName = "openapi.oneof"
76 | inputSpec = file("src/oneOf.yaml")
77 | }
78 | spec("mapResponse") {
79 | packageName = "openapi.mapResponse"
80 | inputSpec = file("src/mapResponse.yaml")
81 | }
82 | spec("AnyType") {
83 | packageName = "openapi.anyType"
84 | inputSpec = file("src/AnyType.yaml")
85 | }
86 | spec("formData") {
87 | packageName = "cases.formData"
88 | inputSpec = file("src/formData.yaml")
89 | }
90 | spec("enumFallbackNull") {
91 | packageName = "cases.enumfallback"
92 | enumFallbackNull = true
93 | inputSpec = file("src/enumFallbackNull.yaml")
94 | }
95 | spec("requestHeader") {
96 | packageName = "openapi.requestHeader"
97 | inputSpec = file("src/requestHeaders.yaml")
98 | }
99 | }
100 |
101 | val copyIosX64TestResources = tasks.register("copyIosX64TestResources") {
102 | from("src/commonTest/resources")
103 | into("build/bin/iosX64/debugTest/resources")
104 | }
105 |
106 | tasks.matching { it.name == "iosX64Test" }.configureEach {
107 | dependsOn(copyIosX64TestResources)
108 | }
109 |
110 | val copyIosArm64TestResources = tasks.register("copyIosArm64TestResources") {
111 | from("src/commonTest/resources")
112 | into("build/bin/iosSimulatorArm64/debugTest/resources")
113 | }
114 |
115 | tasks.matching { it.name == "iosSimulatorArm64Test" }.configureEach {
116 | dependsOn(copyIosArm64TestResources)
117 | }
118 |
119 | tasks.withType()
120 | .matching { it.name.contains("UnitTest") }
121 | .configureEach {
122 | doLast {
123 | val testResourcesDir = File(projectDir, "src/commonTest/resources")
124 | if (testResourcesDir.exists().not()) return@doLast
125 | testResourcesDir.copyRecursively(destinationDirectory.get().asFile, overwrite = true)
126 | }
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlinVersion = "1.8.10"
3 |
4 | # android
5 | lifecycleViewModelVersion = "2.6.1"
6 | glideVersion = "4.14.2"
7 | androidAppCompatVersion = "1.6.1"
8 | coreKtxVersion = "1.10.0"
9 |
10 | # jvm
11 | openApiGeneratorVersion = "5.2.0"
12 | guavaVersion = "30.1-jre"
13 |
14 | # kotlinx
15 | kotlinxSerializationVersion = "1.5.0"
16 | coroutinesVersion = "1.6.4"
17 |
18 | # moko
19 | mokoResourcesVersion = "0.21.2"
20 | mokoMvvmVersion = "0.16.0"
21 | mokoErrorsVersion = "0.7.0"
22 | mokoTestVersion = "0.6.1"
23 | mokoNetworkVersion = "0.21.2"
24 |
25 | # tests
26 | espressoCoreVersion = "3.5.1"
27 | testRunnerVersion = "1.5.0"
28 | testExtJunitVersion = "1.1.5"
29 | androidxTestVersion = "1.5.0"
30 | robolectricVersion = "4.9"
31 |
32 | # other
33 | ktorClientVersion = "2.2.2"
34 | kbignumVersion = "2.4.12"
35 | multidexVersion = "2.0.1"
36 |
37 | [libraries]
38 | # android
39 | appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
40 | glide = { module = "com.github.bumptech.glide:glide", version.ref = "glideVersion" }
41 | lifecycleViewModel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewModelVersion" }
42 | multidex = { module = "androidx.multidex:multidex", version.ref = "multidexVersion" }
43 | coreKtx = { module = "androidx.core:core-ktx", version.ref = "coreKtxVersion" }
44 |
45 | # kotlinx
46 | kotlinSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationVersion" }
47 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
48 |
49 | # ktor
50 | ktorClientOkHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientVersion" }
51 | ktorClient = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientVersion" }
52 | ktorClientLogging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientVersion" }
53 | ktorClientWebSocket = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorClientVersion" }
54 | ktorClientMock = { module = "io.ktor:ktor-client-mock", version.ref = "ktorClientVersion" }
55 | ktorClientIos = { module = "io.ktor:ktor-client-ios", version.ref = "ktorClientVersion" }
56 |
57 | # korlibs
58 | kbignum = { module = "com.soywiz.korlibs.kbignum:kbignum", version.ref = "kbignumVersion" }
59 |
60 | # moko
61 | mokoMvvmDataBinding = { module = "dev.icerock.moko:mvvm-databinding", version.ref = "mokoMvvmVersion" }
62 | mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoResourcesVersion" }
63 | mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" }
64 | mokoMvvmLiveData = { module = "dev.icerock.moko:mvvm-livedata", version.ref = "mokoMvvmVersion" }
65 | mokoErrors = { module = "dev.icerock.moko:errors", version.ref = "mokoErrorsVersion" }
66 |
67 | # tests
68 | espressoCore = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCoreVersion" }
69 | kotlinTestJUnit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
70 | testCore = { module = "androidx.test:core", version.ref = "androidxTestVersion" }
71 | robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectricVersion" }
72 | testRunner = { module = "androidx.test:runner", version.ref = "testRunnerVersion" }
73 | testRules = { module = "androidx.test:rules", version.ref = "testRunnerVersion" }
74 | testExtJunit = { module = "androidx.test.ext:junit", version.ref = "testExtJunitVersion" }
75 | kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinVersion" }
76 | kotlinTestAnnotations = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "kotlinVersion" }
77 | mokoTest = { module = "dev.icerock.moko:test-core", version.ref = "mokoTestVersion" }
78 |
79 | # jvm
80 | openApiGenerator = { module = "org.openapitools:openapi-generator-gradle-plugin", version.ref = "openApiGeneratorVersion" }
81 | guava = { module = "com.google.guava:guava", version.ref = "guavaVersion" }
82 |
83 | # gradle plugins
84 | kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinVersion" }
85 | kotlinSerializationGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlinVersion" }
86 | androidGradlePlugin = { module = "com.android.tools.build:gradle", version = "7.4.2" }
87 | mokoResourcesGradlePlugin = { module = "dev.icerock.moko:resources-generator", version.ref = "mokoResourcesVersion" }
88 | mokoGradlePlugin = { module = "dev.icerock.moko:moko-gradle-plugin", version = "0.3.0" }
89 |
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/SafeableTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.safeable.Safeable
6 | import dev.icerock.moko.network.safeable.SafeableSerializer
7 | import dev.icerock.moko.network.safeable.asSafeable
8 | import kotlinx.serialization.SerialName
9 | import kotlinx.serialization.Serializable
10 | import kotlinx.serialization.SerializationException
11 | import kotlinx.serialization.json.Json
12 | import kotlin.test.BeforeTest
13 | import kotlin.test.Test
14 | import kotlin.test.assertEquals
15 | import kotlin.test.assertFailsWith
16 | import kotlin.test.assertNotNull
17 |
18 | class SafeableTest {
19 |
20 | @BeforeTest
21 | fun setup() {
22 | SafeableSerializer.deserializeExceptionHandler = null
23 | }
24 |
25 | @Test
26 | fun `safeable with default encode`() {
27 | val json = Json.Default
28 | val data = TestData()
29 |
30 | val result = json.encodeToString(TestData.serializer(), data)
31 | assertEquals(expected = "{}", actual = result)
32 | }
33 |
34 | @Test
35 | fun `safeable with null encode`() {
36 | val json = Json.Default
37 | val data = TestData(data = Safeable(value = null))
38 |
39 | assertFailsWith(SerializationException::class) {
40 | json.encodeToString(TestData.serializer(), data)
41 | }
42 | }
43 |
44 | @Test
45 | fun `safeable with value encode`() {
46 | val json = Json.Default
47 | val data = TestData(data = Safeable(value = TestData.TestEnum.ITEM2))
48 |
49 | val result = json.encodeToString(TestData.serializer(), data)
50 | assertEquals(expected = """{"data":"item2"}""", actual = result)
51 | }
52 |
53 | @Test
54 | fun `safeable with default decode`() {
55 | val json = Json.Default
56 | val input = "{}"
57 |
58 | val result = json.decodeFromString(TestData.serializer(), input)
59 | assertEquals(expected = TestData(), actual = result)
60 | }
61 |
62 | @Test
63 | fun `safeable with null decode`() {
64 | val json = Json.Default
65 | val input = """{"data":null}"""
66 |
67 | val result = json.decodeFromString(TestData.serializer(), input)
68 | assertEquals(
69 | expected = TestData(data = null),
70 | actual = result
71 | )
72 | }
73 |
74 | @Test
75 | fun `safeable with value decode`() {
76 | val json = Json.Default
77 | val input = """{"data":"item2"}"""
78 |
79 | val result = json.decodeFromString(TestData.serializer(), input)
80 | assertEquals(
81 | expected = TestData(
82 | data = Safeable(value = TestData.TestEnum.ITEM2)
83 | ),
84 | actual = result
85 | )
86 | }
87 |
88 | @Test
89 | fun `safeable with incorrect value decode`() {
90 | val json = Json.Default
91 | val input = """{"data":"item3"}"""
92 |
93 | val result = json.decodeFromString(TestData.serializer(), input)
94 | assertEquals(
95 | expected = TestData(
96 | data = Safeable(value = null)
97 | ),
98 | actual = result
99 | )
100 | }
101 |
102 | @Test
103 | fun `safeable deserialize handler test`() {
104 | val json = Json.Default
105 | val input = """{"data":"item3"}"""
106 |
107 | var serializationException: SerializationException? = null
108 | SafeableSerializer.deserializeExceptionHandler = {
109 | serializationException = it
110 | true
111 | }
112 |
113 | json.decodeFromString(TestData.serializer(), input)
114 |
115 | assertNotNull(serializationException)
116 | }
117 |
118 | @Test
119 | fun `safeable deserialize handler throws test`() {
120 | val json = Json.Default
121 | val input = """{"data":"item3"}"""
122 |
123 | SafeableSerializer.deserializeExceptionHandler = {
124 | false
125 | }
126 |
127 | assertFailsWith(SerializationException::class) {
128 | json.decodeFromString(TestData.serializer(), input)
129 | }
130 | }
131 |
132 | @Serializable
133 | data class TestData(
134 | val data: Safeable? = TestEnum.ITEM1.asSafeable()
135 | ) {
136 | @Serializable
137 | enum class TestEnum {
138 | @SerialName("item1")
139 | ITEM1,
140 |
141 | @SerialName("item2")
142 | ITEM2
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/network/src/commonTest/kotlin/OneOfTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import dev.icerock.moko.network.exceptions.DataNotFitOneOfSchema
6 | import dev.icerock.moko.network.schemas.ComposedSchemaSerializer
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.json.Json
9 | import kotlinx.serialization.json.JsonElement
10 | import kotlinx.serialization.json.JsonObject
11 | import kotlinx.serialization.json.jsonObject
12 | import kotlin.test.Ignore
13 | import kotlin.test.Test
14 | import kotlin.test.assertEquals
15 | import kotlin.test.assertFailsWith
16 |
17 | // for now oneOf implementation is replace to JsonElement - we can't correctly support oneOf by
18 | // specification examples
19 | // https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof
20 | @Ignore
21 | class OneOfTest {
22 | private val json = Json.Default
23 |
24 | @Test
25 | fun `oneOf decode Doge success`() {
26 | val input = """{"bark":true,"breed":"Dingo"}"""
27 |
28 | val output = json.decodeFromString(OneOfDogCat.serializer(), input)
29 |
30 | assertEquals(
31 | expected = OneOfDogCat(
32 | dog = Doge(bark = true, breed = "Dingo"),
33 | cat = null
34 | ),
35 | actual = output
36 | )
37 | }
38 |
39 | @Test
40 | fun `oneOf decode Cat success`() {
41 | val input = """{"hunts":true}"""
42 |
43 | val output = json.decodeFromString(OneOfDogCat.serializer(), input)
44 |
45 | assertEquals(
46 | expected = OneOfDogCat(
47 | dog = null,
48 | cat = Cat(hunts = true)
49 | ),
50 | actual = output
51 | )
52 | }
53 |
54 | @Test
55 | fun `oneOf decode Doge and Cat fails`() {
56 | val input = """{"bark":true,"hunts":true,"breed":"Husky","age":3}"""
57 |
58 | assertFailsWith(DataNotFitOneOfSchema::class) {
59 | json.decodeFromString(OneOfDogCat.serializer(), input)
60 | }
61 | }
62 |
63 | @Test
64 | fun `oneOf decode invalid data fails`() {
65 | val input = """{"bark":true,"hunts":true}"""
66 |
67 | assertFailsWith(DataNotFitOneOfSchema::class) {
68 | json.decodeFromString(OneOfDogCat.serializer(), input)
69 | }
70 | }
71 |
72 | @Test
73 | fun `oneOf encode Doge`() {
74 | val input = OneOfDogCat(
75 | dog = Doge(bark = true, breed = "Dingo"),
76 | cat = null
77 | )
78 |
79 | val output = json.encodeToString(OneOfDogCat.serializer(), input)
80 |
81 | assertEquals(
82 | expected = """{"bark":true,"breed":"Dingo"}""",
83 | actual = output
84 | )
85 | }
86 |
87 | @Test
88 | fun `oneOf encode Cat`() {
89 | val input = OneOfDogCat(
90 | dog = null,
91 | cat = Cat(hunts = true)
92 | )
93 |
94 | val output = json.encodeToString(OneOfDogCat.serializer(), input)
95 |
96 | assertEquals(
97 | expected = """{"hunts":true}""",
98 | actual = output
99 | )
100 | }
101 | }
102 |
103 |
104 | @Serializable
105 | private data class Doge(
106 | val bark: Boolean? = null,
107 | val breed: String? = null
108 | )
109 |
110 | @Serializable
111 | private data class Cat(
112 | val hunts: Boolean? = null,
113 | val age: Int? = null
114 | )
115 |
116 | @Serializable(with = OneOfDogCatSerializer::class)
117 | private data class OneOfDogCat(
118 | val dog: Doge?,
119 | val cat: Cat?
120 | )
121 |
122 | private object OneOfDogCatSerializer : ComposedSchemaSerializer(
123 | serialName = "OneOfDogCatSerializer"
124 | ) {
125 |
126 | override fun decodeJson(json: Json, element: JsonElement): OneOfDogCat {
127 | val doge = runCatching { json.decodeFromJsonElement(Doge.serializer(), element) }
128 | val cat = runCatching { json.decodeFromJsonElement(Cat.serializer(), element) }
129 |
130 | ensureOnlyOneItemIsSuccess(element, listOf(doge, cat))
131 |
132 | return OneOfDogCat(
133 | dog = doge.getOrNull(),
134 | cat = cat.getOrNull()
135 | )
136 | }
137 |
138 | override fun encodeJson(json: Json, value: OneOfDogCat): List {
139 | val dog = value.dog?.let {
140 | json.encodeToJsonElement(Doge.serializer(), it)
141 | }
142 | val cat = value.cat?.let {
143 | json.encodeToJsonElement(Cat.serializer(), it)
144 | }
145 |
146 | return listOfNotNull(dog, cat).map { it.jsonObject }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/network-errors/src/commonMain/kotlin/dev/icerock/moko/network/errors/NetworkExceptionMappers.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network.errors
6 |
7 | import dev.icerock.moko.errors.MR
8 | import dev.icerock.moko.errors.mappers.ExceptionMappersStorage
9 | import dev.icerock.moko.network.SSLExceptionType
10 | import dev.icerock.moko.network.exceptions.ErrorException
11 | import dev.icerock.moko.network.exceptions.ValidationException
12 | import dev.icerock.moko.network.getSSLExceptionType
13 | import dev.icerock.moko.network.isNetworkConnectionError
14 | import dev.icerock.moko.network.isSSLException
15 | import dev.icerock.moko.resources.desc.CompositionStringDesc
16 | import dev.icerock.moko.resources.desc.ResourceFormatted
17 | import dev.icerock.moko.resources.desc.StringDesc
18 | import dev.icerock.moko.resources.desc.desc
19 | import io.ktor.http.HttpStatusCode
20 | import kotlinx.serialization.SerializationException
21 |
22 | private const val INTERNAL_SERVER_ERROR_CODE_MAX = 599
23 |
24 | /**
25 | * Registers all default exception mappers of the network module to [ExceptionMappersStorage].
26 | */
27 | @Suppress("LongParameterList")
28 | fun ExceptionMappersStorage.registerAllNetworkMappers(
29 | errorsTexts: NetworkErrorsTexts = NetworkErrorsTexts()
30 | ): ExceptionMappersStorage {
31 | return condition(
32 | condition = { it.isNetworkConnectionError() },
33 | mapper = { errorsTexts.networkConnectionErrorText.desc() }
34 | ).condition(
35 | condition = { it.isSSLException() },
36 | mapper = {
37 | getSSLExceptionStringDescMapper(
38 | sslException = it,
39 | sslNetworkErrorsTexts = errorsTexts.sslNetworkErrorsTexts
40 | )
41 | }
42 | ).condition(
43 | condition = { it is SerializationException },
44 | mapper = { errorsTexts.serializationErrorText.desc() }
45 | ).register {
46 | getNetworkErrorExceptionStringDescMapper(
47 | errorException = it,
48 | httpNetworkErrorsTexts = errorsTexts.httpNetworkErrorsTexts
49 | )
50 | }.register(::validationExceptionStringDescMapper)
51 | }
52 |
53 | /**
54 | * Maps the input error [exception] to default error text.
55 | */
56 | private fun getNetworkErrorExceptionStringDescMapper(
57 | errorException: ErrorException,
58 | httpNetworkErrorsTexts: HttpNetworkErrorsTexts
59 | ): StringDesc {
60 | val httpStatusCode = errorException.httpStatusCode
61 | return when {
62 | errorException.isUnauthorized -> httpNetworkErrorsTexts.unauthorizedErrorText.desc()
63 | errorException.isNotFound -> httpNetworkErrorsTexts.notFoundErrorText.desc()
64 | errorException.isAccessDenied -> httpNetworkErrorsTexts.accessDeniedErrorText.desc()
65 | httpStatusCode in HttpStatusCode.InternalServerError.value..INTERNAL_SERVER_ERROR_CODE_MAX -> {
66 | StringDesc.ResourceFormatted(
67 | httpNetworkErrorsTexts.internalServerErrorText,
68 | httpStatusCode
69 | )
70 | }
71 |
72 | else -> MR.strings.moko_errors_unknownError.desc()
73 | }
74 | }
75 |
76 | /**
77 | * Maps the input error [sslException] to ssl error text.
78 | */
79 | @Suppress("ComplexMethod")
80 | private fun getSSLExceptionStringDescMapper(
81 | sslException: Throwable,
82 | sslNetworkErrorsTexts: SSLNetworkErrorsTexts
83 | ): StringDesc {
84 | return when (sslException.getSSLExceptionType()) {
85 | SSLExceptionType.SecureConnectionFailed -> sslNetworkErrorsTexts.secureConnectionFailed.desc()
86 | SSLExceptionType.ServerCertificateHasBadDate -> sslNetworkErrorsTexts.serverCertificateHasBadDate.desc()
87 | SSLExceptionType.ServerCertificateUntrusted -> sslNetworkErrorsTexts.serverCertificateUntrusted.desc()
88 | SSLExceptionType.ServerCertificateHasUnknownRoot -> sslNetworkErrorsTexts.serverCertificateHasUnknownRoot.desc()
89 | SSLExceptionType.ServerCertificateNotYetValid -> sslNetworkErrorsTexts.serverCertificateNotYetValid.desc()
90 | SSLExceptionType.ClientCertificateRejected -> sslNetworkErrorsTexts.clientCertificateRejected.desc()
91 | SSLExceptionType.ClientCertificateRequired -> sslNetworkErrorsTexts.clientCertificateRequired.desc()
92 | SSLExceptionType.CannotLoadFromNetwork -> sslNetworkErrorsTexts.cannotLoadFromNetwork.desc()
93 | else -> MR.strings.moko_errors_unknownError.desc()
94 | }
95 | }
96 |
97 | /**
98 | * Converts the validation [exception] to a combination of messages as one [CompositionStringDesc].
99 | */
100 | private fun validationExceptionStringDescMapper(exception: ValidationException): StringDesc {
101 | return exception.errors.joinToString(separator = ". ") { it.message }.desc()
102 | }
103 |
--------------------------------------------------------------------------------
/network-generator/src/main/kotlin/dev/icerock/moko/network/ComposedSchemaProcessor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | package dev.icerock.moko.network
6 |
7 | import io.swagger.v3.oas.models.OpenAPI
8 | import io.swagger.v3.oas.models.Operation
9 | import io.swagger.v3.oas.models.media.ComposedSchema
10 | import io.swagger.v3.oas.models.media.Schema
11 |
12 | internal class ComposedSchemaProcessor(
13 | private val operationIdGenerator: (operation: Operation, path: String, method: String) -> String
14 | ) : OpenApiSchemaProcessor {
15 |
16 | @Suppress("ReturnCount")
17 | override fun process(openApi: OpenAPI, schema: Schema<*>, context: SchemaContext): Schema<*> {
18 | if (schema !is ComposedSchema) return schema
19 |
20 | if (schema.allOf != null) {
21 | return processAllOfSchema(
22 | schemas = openApi.components.schemas,
23 | schema = schema,
24 | context = context,
25 | allOfSchemas = schema.allOf
26 | )
27 | }
28 | if (schema.anyOf != null) {
29 | return processAnyOfSchema(
30 | schemas = openApi.components.schemas,
31 | schema = schema,
32 | context = context,
33 | anyOfSchemas = schema.anyOf
34 | )
35 | }
36 |
37 | return schema
38 | }
39 |
40 | private fun processAllOfSchema(
41 | schemas: MutableMap>,
42 | schema: ComposedSchema,
43 | context: SchemaContext,
44 | allOfSchemas: List>
45 | ): Schema<*> {
46 | if (allOfSchemas.size == 1) return allOfSchemas.first().withPropsOf(schema)
47 |
48 | val newSchemaName: String = context.buildSchemaName() + "_composed"
49 | extractSchema(
50 | schemas = schemas,
51 | newSchemaName = newSchemaName,
52 | markExtension = "x-allOfGeneration",
53 | propertySchemas = allOfSchemas
54 | )
55 |
56 | return Schema().apply {
57 | `$ref` = "#/components/schemas/$newSchemaName"
58 | }
59 | }
60 |
61 | private fun processAnyOfSchema(
62 | schemas: MutableMap>,
63 | schema: ComposedSchema,
64 | context: SchemaContext,
65 | anyOfSchemas: List>
66 | ): Schema<*> {
67 | if (anyOfSchemas.size == 1) return anyOfSchemas.first().withPropsOf(schema)
68 |
69 | val newSchemaName: String = context.buildSchemaName() + "_composed"
70 | extractSchema(
71 | schemas = schemas,
72 | newSchemaName = newSchemaName,
73 | markExtension = "x-anyOfGeneration",
74 | propertySchemas = anyOfSchemas
75 | )
76 |
77 | return Schema().apply {
78 | `$ref` = "#/components/schemas/$newSchemaName"
79 | }
80 | }
81 |
82 | private fun extractSchema(
83 | schemas: MutableMap>,
84 | newSchemaName: String,
85 | markExtension: String,
86 | propertySchemas: List>
87 | ) {
88 | val resultSchema = Schema().apply {
89 | addExtension(markExtension, true)
90 | properties = mutableMapOf()
91 | }
92 | schemas[newSchemaName] = resultSchema
93 |
94 | var inlineIdx = 1
95 | propertySchemas.forEachIndexed { index, schema ->
96 | val propertyName = "item_$index"
97 |
98 | if (schema.`$ref` == null) {
99 | val name = newSchemaName + "_inline_" + inlineIdx
100 | inlineIdx++
101 | if (schemas.containsKey(name)) throw IllegalAccessException(name)
102 |
103 | schemas[name] = schema
104 |
105 | resultSchema.properties[propertyName] = Schema().apply {
106 | `$ref` = "#/components/schemas/$name"
107 | }
108 | } else {
109 | resultSchema.properties[propertyName] = schema
110 | }
111 | }
112 | }
113 |
114 | private fun Schema<*>.withPropsOf(schema: ComposedSchema) = apply {
115 | nullable = schema.nullable
116 | }
117 |
118 | private fun SchemaContext.buildSchemaName(): String {
119 | return when (this) {
120 | is SchemaContext.OperationResponse -> {
121 | operationIdGenerator(
122 | operation,
123 | pathName,
124 | method.name.lowercase().capitalize()
125 | ) + "_response_" + this.responseName
126 | }
127 | is SchemaContext.Response -> this.responseName
128 | is SchemaContext.Request -> this.requestName
129 | is SchemaContext.OperationRequest -> operationIdGenerator(
130 | operation,
131 | pathName,
132 | method.name.lowercase().capitalize()
133 | ) + "_requestBody"
134 | is SchemaContext.ParameterComponent -> this.parameterName
135 | is SchemaContext.SchemaComponent -> this.schemaName
136 | is SchemaContext.PropertyComponent -> this.schemaName.orEmpty() + "_" + this.propertyName
137 | is SchemaContext.Child -> this.parent.buildSchemaName() + "_" + this.child.buildSchemaName()
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/sample/mpp-library/src/commonTest/kotlin/EnumFallbackNullTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
3 | */
4 |
5 | import cases.enumfallback.apis.DefaultApi
6 | import cases.enumfallback.models.CarColor
7 | import cases.enumfallback.models.CarColorDefault
8 | import cases.enumfallback.models.CarColorList
9 | import cases.enumfallback.models.CarColorNullable
10 | import cases.enumfallback.models.CarColorRequired
11 | import dev.icerock.moko.network.safeable.extractSafeables
12 | import io.ktor.client.engine.mock.MockRequestHandler
13 | import io.ktor.client.engine.mock.respondOk
14 | import kotlinx.coroutines.runBlocking
15 | import kotlinx.serialization.json.Json
16 | import kotlin.test.Test
17 | import kotlin.test.assertEquals
18 | import kotlin.test.assertNotNull
19 | import kotlin.test.assertNull
20 | import kotlin.test.assertTrue
21 |
22 | class EnumFallbackNullTest {
23 | @Test
24 | fun `enum fallback - expected response`() {
25 | val enumFallbackApi = createEnumFallbackNullApi { request ->
26 | respondOk(
27 | """{"color":"red"}"""
28 | )
29 | }
30 |
31 | val result = runBlocking {
32 | enumFallbackApi.carColors()
33 | }
34 |
35 | // Assert CarColor
36 | assertNotNull(result.item0?.color?.value)
37 | assertEquals(CarColor.Color.RED, result.item0?.color?.value)
38 |
39 | // Assert CarColorDefault
40 | assertNotNull(result.item1?.color?.value)
41 | assertEquals(CarColorDefault.Color.RED, result.item1?.color?.value)
42 |
43 | // Assert CarColorRequired
44 | assertNotNull(result.item2?.color?.value)
45 | assertEquals(CarColorRequired.Color.RED, result.item2?.color?.value)
46 |
47 | // Assert CarColorNullable
48 | assertNotNull(result.item3?.color?.value?.value)
49 | assertEquals(CarColorNullable.Color.RED, result.item3?.color?.value?.value)
50 | }
51 |
52 | @Test
53 | fun `enum fallback - expected list response`() {
54 | val enumFallbackApi = createEnumFallbackNullApi { request ->
55 | respondOk(
56 | """{"color": [ "red", "white" ] }"""
57 | )
58 | }
59 |
60 | val result = runBlocking {
61 | enumFallbackApi.carColorsList()
62 | }
63 |
64 | assertNotNull(result.color)
65 | assertTrue { result.color.size == 2 }
66 | assertTrue { result.color.extractSafeables().contains(CarColorList.Color.RED) }
67 | assertTrue { result.color.extractSafeables().contains(CarColorList.Color.WHITE) }
68 | }
69 |
70 | @Test
71 | fun `enum fallback - unexpected response`() {
72 | val enumFallbackApi = createEnumFallbackNullApi { request ->
73 | respondOk(
74 | """{"color":"cyan"}"""
75 | )
76 | }
77 |
78 | val result = runBlocking {
79 | enumFallbackApi.carColors()
80 | }
81 |
82 | // Assert CarColor
83 | assertNotNull(result.item0?.color)
84 | assertNull(result.item0?.color?.value)
85 |
86 | // Assert CarColorDefault
87 | assertNotNull(result.item1?.color)
88 | assertNull(result.item1?.color?.value)
89 |
90 | // Assert CarColorRequired
91 | assertNotNull(result.item2?.color)
92 | assertNull(result.item2?.color?.value)
93 |
94 | // Assert CarColorNullable
95 | assertNotNull(result.item3?.color?.value)
96 | assertNull(result.item3?.color?.value?.value)
97 | }
98 |
99 | @Test
100 | fun `enum fallback - unexpected list response`() {
101 | val enumFallbackApi = createEnumFallbackNullApi { request ->
102 | respondOk(
103 | """{"color": [ "cyan", "red" ] }"""
104 | )
105 | }
106 |
107 | val result = runBlocking {
108 | enumFallbackApi.carColorsList()
109 | }
110 |
111 | assertNotNull(result.color)
112 | assertTrue { result.color.size == 2 }
113 | assertTrue { result.color.extractSafeables().contains(CarColorList.Color.RED) }
114 | }
115 |
116 | @Test
117 | fun `enum fallback - unexpected key`() {
118 | val enumFallbackApi = createEnumFallbackNullApi { request ->
119 | respondOk(
120 | """{"number":1020}"""
121 | )
122 | }
123 |
124 | val result = runBlocking {
125 | enumFallbackApi.carColors()
126 | }
127 |
128 | // Assert CarColor
129 | assertNull(result.item0?.color)
130 |
131 | // Assert CarColorDefault
132 | assertNotNull(result.item1?.color)
133 | assertEquals(
134 | expected = CarColorDefault.Color.RED,
135 | actual = result.item1?.color?.value
136 | )
137 |
138 | // Assert CarColorRequired
139 | assertNull(result.item2)
140 |
141 | // Assert CarColorNullable
142 | assertNull(result.item3?.color)
143 | }
144 |
145 | private fun createEnumFallbackNullApi(mock: MockRequestHandler): DefaultApi {
146 | val json = Json {
147 | coerceInputValues = true
148 | ignoreUnknownKeys = true
149 | }
150 | val httpClient = createMockClient(json, mock)
151 | return DefaultApi(
152 | basePath = "https://localhost",
153 | httpClient = httpClient,
154 | json = json
155 | )
156 | }
157 | }
158 |
--------------------------------------------------------------------------------