From 0093669eb956958435ce829260cdf79b6415afee Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Wed, 26 Apr 2023 22:21:49 -0300 Subject: [PATCH 1/4] refactor: Move network infra and utils to :ychat-core --- settings.gradle.kts | 3 +- ychat-core/build.gradle.kts | 87 +++++++++++++++++++ .../co/yml/ychat/core}/model/FileBytes.kt | 2 +- .../network/factories/HttpEngineFactory.kt | 10 +++ .../ychat/core/exceptions/YChatException.kt | 4 +- .../co/yml/ychat/core/model/FileBytes.kt | 5 ++ .../network/extensions/ApiResultExtensions.kt | 36 ++++++++ .../network/factories/HttpClientFactory.kt | 7 ++ .../network/factories/HttpEngineFactory.kt | 7 ++ .../network}/infrastructure/ApiExecutor.kt | 17 ++-- .../core/network}/infrastructure/ApiResult.kt | 14 +-- .../yml/ychat/core}/storage/ChatLogStorage.kt | 4 +- .../co/yml/ychat/core}/model/FileBytes.kt | 2 +- .../network/factories/HttpEngineFactory.kt | 11 +++ .../co/yml/ychat/core/model/FileBytes.kt | 7 ++ .../network/factories/HttpEngineFactory.kt | 11 +++ ychat/build.gradle.kts | 21 +---- .../co/yml/ychat/di/module/platformModule.kt | 8 -- .../co/yml/ychat/data/api/ChatGptApi.kt | 2 +- .../yml/ychat/data/api/impl/ChatGptApiImpl.kt | 6 +- .../co/yml/ychat/data/dto/AudioParamsDto.kt | 2 +- .../infrastructure/ApiResultExtensions.kt | 33 ------- .../infrastructure/OpenAiHttpClient.kt} | 21 +++-- .../co/yml/ychat/di/module/LibraryModule.kt | 13 ++- .../co/yml/ychat/domain/mapper/AudioMapper.kt | 2 +- .../co/yml/ychat/domain/model/FileBytes.kt | 5 -- .../yml/ychat/domain/usecases/AudioUseCase.kt | 2 +- .../domain/usecases/CompletionUseCase.kt | 4 +- .../features/AudioTranscriptions.kt | 8 +- .../entrypoint/features/AudioTranslations.kt | 8 +- .../entrypoint/features/ChatCompletions.kt | 6 +- .../ychat/entrypoint/features/Completion.kt | 4 +- .../co/yml/ychat/entrypoint/features/Edits.kt | 6 +- .../entrypoint/features/ImageGenerations.kt | 6 +- .../ychat/entrypoint/features/ListModels.kt | 6 +- .../entrypoint/features/RetrieveModel.kt | 6 +- .../impl/AudioTranscriptionsImpl.kt | 2 +- .../entrypoint/impl/AudioTranslationsImpl.kt | 2 +- .../co/yml/ychat/di/module/platformModule.kt | 8 -- .../co/yml/ychat/di/module/platformModule.kt | 8 -- .../co/yml/ychat/domain/model/FileBytes.kt | 7 -- 41 files changed, 265 insertions(+), 158 deletions(-) create mode 100644 ychat-core/build.gradle.kts rename {ychat/src/androidMain/kotlin/co/yml/ychat/domain => ychat-core/src/androidMain/kotlin/co/yml/ychat/core}/model/FileBytes.kt (75%) create mode 100644 ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt rename ychat/src/commonMain/kotlin/co/yml/ychat/data/exception/ChatGptException.kt => ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt (81%) create mode 100644 ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt create mode 100644 ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt create mode 100644 ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt create mode 100644 ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt rename {ychat/src/commonMain/kotlin/co/yml/ychat/data => ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network}/infrastructure/ApiExecutor.kt (83%) rename {ychat/src/commonMain/kotlin/co/yml/ychat/data => ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network}/infrastructure/ApiResult.kt (55%) rename {ychat/src/commonMain/kotlin/co/yml/ychat/data => ychat-core/src/commonMain/kotlin/co/yml/ychat/core}/storage/ChatLogStorage.kt (86%) rename {ychat/src/iosMain/kotlin/co/yml/ychat/domain => ychat-core/src/iosMain/kotlin/co/yml/ychat/core}/model/FileBytes.kt (92%) create mode 100644 ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt create mode 100644 ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt create mode 100644 ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt delete mode 100644 ychat/src/androidMain/kotlin/co/yml/ychat/di/module/platformModule.kt delete mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResultExtensions.kt rename ychat/src/commonMain/kotlin/co/yml/ychat/{di/provider/NetworkProvider.kt => data/infrastructure/OpenAiHttpClient.kt} (69%) delete mode 100644 ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt delete mode 100644 ychat/src/iosMain/kotlin/co/yml/ychat/di/module/platformModule.kt delete mode 100644 ychat/src/jvmMain/kotlin/co/yml/ychat/di/module/platformModule.kt delete mode 100644 ychat/src/jvmMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 30a2a59..c0f70f5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,4 +17,5 @@ dependencyResolutionManagement { rootProject.name = "ychat-sdk" include(":ychat") include(":sample:android") -include(":sample:jvm") \ No newline at end of file +include(":sample:jvm") +include(":ychat-core") diff --git a/ychat-core/build.gradle.kts b/ychat-core/build.gradle.kts new file mode 100644 index 0000000..f3d84af --- /dev/null +++ b/ychat-core/build.gradle.kts @@ -0,0 +1,87 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") + id("io.gitlab.arturbosch.detekt") + id("org.jlleitschuh.gradle.ktlint") + id("org.jetbrains.kotlinx.kover") +} + +kover { + verify { + rule { + name = "Minimal line coverage rate in percents" + bound { + minValue = 90 + } + } + } +} + +kotlin { + android() + jvm() + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ) + + sourceSets { + val commonMain by getting { + dependencies { + api(Dependencies.Network.KTOR_NEGOTIATION) + api(Dependencies.Network.KTOR_SERIALIZATION) + api(Dependencies.Network.KTOR_CORE) + api(Dependencies.Network.KTOR_LOGGING) + api(Dependencies.DI.KOIN_CORE) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(Dependencies.Test.MOCKK_COMMON) + implementation(Dependencies.Test.KTOR) + implementation(Dependencies.Test.KOIN) + } + } + val androidMain by getting { + dependencies { + implementation(Dependencies.Network.KTOR_OKHTTP) + } + } + val iosX64Main by getting + val iosArm64Main by getting + val iosSimulatorArm64Main by getting + val iosMain by creating { + dependsOn(commonMain) + iosX64Main.dependsOn(this) + iosArm64Main.dependsOn(this) + iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation(Dependencies.Network.KTOR_IOS) + } + } + val iosTest by creating { + dependsOn(commonTest) + } + val jvmMain by getting { + dependencies { + implementation(Dependencies.Network.KTOR_OKHTTP) + } + } + val jvmTest by getting { + dependencies { + implementation(Dependencies.Test.MOCKK_JVM) + } + } + } +} + +android { + namespace = "co.yml.ychat.core" + compileSdk = Config.COMPILE_SDK_VERSION + defaultConfig { + minSdk = Config.MIN_SDK_VERSION + targetSdk = Config.TARGET_SDK_VERSION + } +} diff --git a/ychat/src/androidMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt similarity index 75% rename from ychat/src/androidMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt rename to ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index eecc7a7..d75fd32 100644 --- a/ychat/src/androidMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt +++ b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -1,4 +1,4 @@ -package co.yml.ychat.domain.model +package co.yml.ychat.core.model actual typealias FileBytes = ByteArray diff --git a/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt new file mode 100644 index 0000000..c9d1360 --- /dev/null +++ b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -0,0 +1,10 @@ +package co.yml.ychat.core.network.factories + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.okhttp.OkHttp + +actual object HttpEngineFactory { + actual fun getEngine(): HttpClientEngine { + return OkHttp.create() + } +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/exception/ChatGptException.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt similarity index 81% rename from ychat/src/commonMain/kotlin/co/yml/ychat/data/exception/ChatGptException.kt rename to ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt index 1e044c9..ad03b96 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/exception/ChatGptException.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt @@ -1,6 +1,6 @@ -package co.yml.ychat.data.exception +package co.yml.ychat.core.exceptions -class ChatGptException( +class YChatException( message: String? = null, cause: Throwable? = null, var statusCode: Int? = null, diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt new file mode 100644 index 0000000..83960ca --- /dev/null +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -0,0 +1,5 @@ +package co.yml.ychat.core.model + +expect class FileBytes + +expect fun FileBytes.toByteArray(): ByteArray diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt new file mode 100644 index 0000000..018505c --- /dev/null +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt @@ -0,0 +1,36 @@ +package co.yml.ychat.core.network.extensions + +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult +import io.ktor.client.call.body +import io.ktor.client.plugins.ResponseException +import io.ktor.client.statement.HttpResponse +import io.ktor.client.statement.bodyAsText +import io.ktor.http.isSuccess +import io.ktor.util.toMap + +suspend inline fun HttpResponse.toApiResult(): ApiResult { + val headers = this.headers.toMap() + val statusCode = this.status.value + return if (!this.status.isSuccess()) { + val errorMessage = this.bodyAsText() + val exception = YChatException(errorMessage, statusCode = statusCode) + ApiResult(null, headers, statusCode, exception) + } else { + ApiResult(this.body(), headers, statusCode, null) + } +} + +fun ResponseException.toApiResult(): ApiResult { + return ApiResult( + statusCode = this.response.status.value, + exception = YChatException(this.cause, this.response.status.value) + ) +} + +fun Throwable.toApiResult(): ApiResult { + return ApiResult( + statusCode = null, + exception = YChatException(this.cause) + ) +} diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt new file mode 100644 index 0000000..104b706 --- /dev/null +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt @@ -0,0 +1,7 @@ +package co.yml.ychat.core.network.factories + +import io.ktor.client.HttpClient + +interface HttpClientFactory { + fun getHttpClient(): HttpClient +} diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt new file mode 100644 index 0000000..f82d35c --- /dev/null +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -0,0 +1,7 @@ +package co.yml.ychat.core.network.factories + +import io.ktor.client.engine.HttpClientEngine + +expect object HttpEngineFactory { + actual fun getEngine(): HttpClientEngine +} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiExecutor.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt similarity index 83% rename from ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiExecutor.kt rename to ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt index 0917690..ebeb448 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiExecutor.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt @@ -1,6 +1,7 @@ -package co.yml.ychat.data.infrastructure +package co.yml.ychat.core.network.infrastructure -import io.ktor.client.HttpClient +import co.yml.ychat.core.network.extensions.toApiResult +import co.yml.ychat.core.network.factories.HttpClientFactory import io.ktor.client.plugins.ResponseException import io.ktor.client.request.forms.FormPart import io.ktor.client.request.forms.formData @@ -16,7 +17,9 @@ import io.ktor.http.HttpMethod import io.ktor.utils.io.errors.IOException import kotlin.collections.set -internal class ApiExecutor(private val httpClient: HttpClient) { +class ApiExecutor(private val httpClientFactory: HttpClientFactory) { + + private val httpClient by lazy { httpClientFactory.getHttpClient() } private lateinit var endpoint: String @@ -26,7 +29,7 @@ internal class ApiExecutor(private val httpClient: HttpClient) { private var query: HashMap = HashMap() - private val formParts = mutableListOf>() + val formParts = mutableListOf>() fun setEndpoint(endpoint: String): ApiExecutor { this.endpoint = endpoint @@ -78,15 +81,15 @@ internal class ApiExecutor(private val httpClient: HttpClient) { } } - private suspend fun executeRequest(): HttpResponse { + suspend fun executeRequest(): HttpResponse { return httpClient.request(endpoint) { url { query.forEach { parameters.append(it.key, it.value) } } method = httpMethod - setBody(this@ApiExecutor.body) + this.setBody(this@ApiExecutor.body) } } - private suspend fun executeRequestAsForm(): HttpResponse { + suspend fun executeRequestAsForm(): HttpResponse { return httpClient.submitFormWithBinaryData( url = endpoint, formData = formData { formParts.forEach { append(it) } } diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResult.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt similarity index 55% rename from ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResult.kt rename to ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt index a855737..d8506d9 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResult.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt @@ -1,28 +1,28 @@ -package co.yml.ychat.data.infrastructure +package co.yml.ychat.core.network.infrastructure -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException /** Encapsulates an outcome from source api. */ -internal data class ApiResult( +data class ApiResult( val body: T? = null, val headers: Map> = mapOf(), val statusCode: Int? = null, - val exception: ChatGptException? = null, + val exception: YChatException? = null, ) { /** Return true if the api outcome was successful. */ val isSuccessful: Boolean = exception == null - /** Try to get [body], if it is null an [ChatGptException] will be thrown. */ + /** Try to get [body], if it is null an [YChatException] will be thrown. */ fun getBodyOrThrow(): T { val exception = exception - ?: ChatGptException("Could not retrieve body from ApiResult.") + ?: YChatException("Could not retrieve body from ApiResult.") return body ?: throw exception } /** Throw an [exception] when [isSuccessful] is false. */ fun ensureSuccess() { if (!isSuccessful) - throw exception ?: ChatGptException("Api request was not successful.") + throw exception ?: YChatException("Api request was not successful.") } } diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/storage/ChatLogStorage.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt similarity index 86% rename from ychat/src/commonMain/kotlin/co/yml/ychat/data/storage/ChatLogStorage.kt rename to ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt index ba28041..381c9d8 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/storage/ChatLogStorage.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt @@ -1,6 +1,6 @@ -package co.yml.ychat.data.storage +package co.yml.ychat.core.storage -internal class ChatLogStorage { +class ChatLogStorage { private val chatLog: MutableList = mutableListOf() diff --git a/ychat/src/iosMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt similarity index 92% rename from ychat/src/iosMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt rename to ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index 4edf13b..28a4e82 100644 --- a/ychat/src/iosMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt +++ b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -1,4 +1,4 @@ -package co.yml.ychat.domain.model +package co.yml.ychat.core.model import kotlinx.cinterop.addressOf import kotlinx.cinterop.usePinned diff --git a/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt new file mode 100644 index 0000000..4f9c4c8 --- /dev/null +++ b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -0,0 +1,11 @@ +package co.yml.ychat.core.network.factories + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.darwin.Darwin + +actual object HttpEngineFactory { + + actual fun getEngine(): HttpClientEngine { + return Darwin.create() + } +} diff --git a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt new file mode 100644 index 0000000..d75fd32 --- /dev/null +++ b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -0,0 +1,7 @@ +package co.yml.ychat.core.model + +actual typealias FileBytes = ByteArray + +actual fun FileBytes.toByteArray(): ByteArray { + return this +} diff --git a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt new file mode 100644 index 0000000..73b4646 --- /dev/null +++ b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -0,0 +1,11 @@ +package co.yml.ychat.core.network.factories + +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.okhttp.OkHttp + +actual object HttpEngineFactory { + + actual fun getEngine(): HttpClientEngine { + return OkHttp.create() + } +} diff --git a/ychat/build.gradle.kts b/ychat/build.gradle.kts index caf5a18..1a1f8ff 100644 --- a/ychat/build.gradle.kts +++ b/ychat/build.gradle.kts @@ -50,11 +50,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(Dependencies.Network.KTOR_NEGOTIATION) - implementation(Dependencies.Network.KTOR_SERIALIZATION) - implementation(Dependencies.Network.KTOR_CORE) - implementation(Dependencies.Network.KTOR_LOGGING) - implementation(Dependencies.DI.KOIN_CORE) + implementation(project(":ychat-core")) } } val commonTest by getting { @@ -65,11 +61,7 @@ kotlin { implementation(Dependencies.Test.KOIN) } } - val androidMain by getting { - dependencies { - implementation(Dependencies.Network.KTOR_OKHTTP) - } - } + val androidMain by getting val androidTest by getting { dependencies { implementation(Dependencies.Test.MOCKK) @@ -83,9 +75,6 @@ kotlin { iosX64Main.dependsOn(this) iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) - dependencies { - implementation(Dependencies.Network.KTOR_IOS) - } } val iosX64Test by getting val iosArm64Test by getting @@ -96,11 +85,7 @@ kotlin { iosArm64Test.dependsOn(this) iosSimulatorArm64Test.dependsOn(this) } - val jvmMain by getting { - dependencies { - implementation(Dependencies.Network.KTOR_OKHTTP) - } - } + val jvmMain by getting val jvmTest by getting { dependencies { implementation(Dependencies.Test.MOCKK_JVM) diff --git a/ychat/src/androidMain/kotlin/co/yml/ychat/di/module/platformModule.kt b/ychat/src/androidMain/kotlin/co/yml/ychat/di/module/platformModule.kt deleted file mode 100644 index 57f48fe..0000000 --- a/ychat/src/androidMain/kotlin/co/yml/ychat/di/module/platformModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package co.yml.ychat.di.module - -import io.ktor.client.engine.okhttp.OkHttp -import org.koin.dsl.module - -internal actual fun platformModule() = module { - single { OkHttp.create() } -} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt index 4b4b710..0ff5385 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/ChatGptApi.kt @@ -1,5 +1,6 @@ package co.yml.ychat.data.api +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.dto.AudioParamsDto import co.yml.ychat.data.dto.AudioResultDto import co.yml.ychat.data.dto.ChatCompletionParamsDto @@ -12,7 +13,6 @@ import co.yml.ychat.data.dto.ImageGenerationsDto import co.yml.ychat.data.dto.ImageGenerationsParamsDto import co.yml.ychat.data.dto.ModelDto import co.yml.ychat.data.dto.ModelListDto -import co.yml.ychat.data.infrastructure.ApiResult internal interface ChatGptApi { diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt index f224505..c24f1a9 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/api/impl/ChatGptApiImpl.kt @@ -1,5 +1,8 @@ package co.yml.ychat.data.api.impl +import co.yml.ychat.core.model.toByteArray +import co.yml.ychat.core.network.infrastructure.ApiExecutor +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.AudioParamsDto import co.yml.ychat.data.dto.AudioResultDto @@ -13,9 +16,6 @@ import co.yml.ychat.data.dto.ImageGenerationsDto import co.yml.ychat.data.dto.ImageGenerationsParamsDto import co.yml.ychat.data.dto.ModelDto import co.yml.ychat.data.dto.ModelListDto -import co.yml.ychat.data.infrastructure.ApiExecutor -import co.yml.ychat.data.infrastructure.ApiResult -import co.yml.ychat.domain.model.toByteArray import io.ktor.http.HttpMethod internal class ChatGptApiImpl(private val apiExecutor: ApiExecutor) : ChatGptApi { diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/AudioParamsDto.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/AudioParamsDto.kt index 2396d19..198d096 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/AudioParamsDto.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/dto/AudioParamsDto.kt @@ -1,6 +1,6 @@ package co.yml.ychat.data.dto -import co.yml.ychat.domain.model.FileBytes +import co.yml.ychat.core.model.FileBytes internal data class AudioParamsDto( val filename: String, diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResultExtensions.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResultExtensions.kt deleted file mode 100644 index 9c7d8f2..0000000 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/ApiResultExtensions.kt +++ /dev/null @@ -1,33 +0,0 @@ -package co.yml.ychat.data.infrastructure - -import co.yml.ychat.data.exception.ChatGptException -import io.ktor.client.call.body -import io.ktor.client.plugins.ResponseException -import io.ktor.client.statement.HttpResponse -import io.ktor.http.isSuccess -import io.ktor.util.toMap - -internal suspend inline fun HttpResponse.toApiResult(): ApiResult { - val headers = this.headers.toMap() - val statusCode = this.status.value - return if (!this.status.isSuccess()) { - val exception = ChatGptException(null, statusCode) - ApiResult(null, headers, statusCode, exception) - } else { - ApiResult(this.body(), headers, statusCode, null) - } -} - -internal fun ResponseException.toApiResult(): ApiResult { - return ApiResult( - statusCode = this.response.status.value, - exception = ChatGptException(this.cause, this.response.status.value) - ) -} - -internal fun Throwable.toApiResult(): ApiResult { - return ApiResult( - statusCode = null, - exception = ChatGptException(this.cause) - ) -} diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/di/provider/NetworkProvider.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClient.kt similarity index 69% rename from ychat/src/commonMain/kotlin/co/yml/ychat/di/provider/NetworkProvider.kt rename to ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClient.kt index 5cbec0e..db0ffb1 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/di/provider/NetworkProvider.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClient.kt @@ -1,5 +1,7 @@ -package co.yml.ychat.di.provider +package co.yml.ychat.data.infrastructure +import co.yml.ychat.core.network.factories.HttpClientFactory +import co.yml.ychat.core.network.factories.HttpEngineFactory import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.HttpTimeout @@ -12,13 +14,13 @@ import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json -internal object NetworkProvider { +internal class OpenAiHttpClient( + private val apiKey: String, + private val httpEngine: HttpClientEngine = HttpEngineFactory.getEngine() +) : HttpClientFactory { - private const val BASE_URL = "api.openai.com" - private const val TIMEOUT_MILLIS = 60000L - - fun provideHttpClient(engine: HttpClientEngine, apiKey: String): HttpClient { - return HttpClient(engine) { + override fun getHttpClient(): HttpClient { + return HttpClient(httpEngine) { defaultRequest { url { host = BASE_URL @@ -43,4 +45,9 @@ internal object NetworkProvider { } } } + + companion object { + private const val BASE_URL = "api.openai.com" + private const val TIMEOUT_MILLIS = 60000L + } } diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt index 3ae8402..186b432 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/di/module/LibraryModule.kt @@ -1,10 +1,11 @@ package co.yml.ychat.di.module +import co.yml.ychat.core.network.factories.HttpClientFactory +import co.yml.ychat.core.network.infrastructure.ApiExecutor import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.api.impl.ChatGptApiImpl -import co.yml.ychat.data.infrastructure.ApiExecutor -import co.yml.ychat.data.storage.ChatLogStorage -import co.yml.ychat.di.provider.NetworkProvider +import co.yml.ychat.data.infrastructure.OpenAiHttpClient +import co.yml.ychat.core.storage.ChatLogStorage import co.yml.ychat.domain.usecases.AudioUseCase import co.yml.ychat.domain.usecases.ChatCompletionsUseCase import co.yml.ychat.domain.usecases.CompletionUseCase @@ -35,7 +36,7 @@ import org.koin.dsl.module internal class LibraryModule(private val apiKey: String) { fun modules(): List = - entrypointModule + domainModule + dataModule + platformModule() + entrypointModule + domainModule + dataModule private val entrypointModule = module { factory { ListModelsImpl(Dispatchers.Default, get()) } @@ -59,11 +60,9 @@ internal class LibraryModule(private val apiKey: String) { } private val dataModule = module { - single { NetworkProvider.provideHttpClient(get(), apiKey) } + single { OpenAiHttpClient(apiKey) } single { ChatLogStorage() } factory { ApiExecutor(get()) } factory { ChatGptApiImpl(get()) } } } - -internal expect fun platformModule(): Module diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/AudioMapper.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/AudioMapper.kt index af8f070..3c24730 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/AudioMapper.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/mapper/AudioMapper.kt @@ -1,8 +1,8 @@ package co.yml.ychat.domain.mapper +import co.yml.ychat.core.model.FileBytes import co.yml.ychat.data.dto.AudioParamsDto import co.yml.ychat.domain.model.AudioParams -import co.yml.ychat.domain.model.FileBytes internal fun AudioParams.toAudioParamsDto(filename: String, fileBytes: FileBytes): AudioParamsDto { return AudioParamsDto( diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt deleted file mode 100644 index 8365d80..0000000 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.yml.ychat.domain.model - -expect class FileBytes - -internal expect fun FileBytes.toByteArray(): ByteArray diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/AudioUseCase.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/AudioUseCase.kt index a7300ac..c9ba5e9 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/AudioUseCase.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/AudioUseCase.kt @@ -1,9 +1,9 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.model.FileBytes import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.domain.mapper.toAudioParamsDto import co.yml.ychat.domain.model.AudioParams -import co.yml.ychat.domain.model.FileBytes internal class AudioUseCase(private val chatGptApi: ChatGptApi) { diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/CompletionUseCase.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/CompletionUseCase.kt index aa0bc68..8b3aba2 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/CompletionUseCase.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/domain/usecases/CompletionUseCase.kt @@ -1,9 +1,9 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.CompletionDto -import co.yml.ychat.data.infrastructure.ApiResult -import co.yml.ychat.data.storage.ChatLogStorage +import co.yml.ychat.core.storage.ChatLogStorage import co.yml.ychat.domain.mapper.toCompletionModel import co.yml.ychat.domain.mapper.toCompletionParamsDto import co.yml.ychat.domain.model.CompletionModel diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranscriptions.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranscriptions.kt index d34ff5b..257fd20 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranscriptions.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranscriptions.kt @@ -1,8 +1,8 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.domain.model.FileBytes +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.model.FileBytes import kotlin.coroutines.cancellation.CancellationException interface AudioTranscriptions { @@ -54,9 +54,9 @@ interface AudioTranscriptions { * @param audioFile The audio file to transcribe, in one of these formats: mp3, mp4, mpeg, mpga, m4a, wav, or webm. * @return The transcript output in the specified format. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error transcribing the audio file. + * @throws YChatException if there is an error transcribing the audio file. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(filename: String, audioFile: FileBytes): String /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranslations.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranslations.kt index a634a8c..7f9a69b 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranslations.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/AudioTranslations.kt @@ -1,8 +1,8 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.domain.model.FileBytes +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.model.FileBytes import kotlin.coroutines.cancellation.CancellationException interface AudioTranslations { @@ -47,9 +47,9 @@ interface AudioTranslations { * mpga, m4a, wav, or webm. * @return The transcript output in the specified format. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error transcribing the audio file. + * @throws YChatException if there is an error transcribing the audio file. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(filename: String, audioFile: FileBytes): String /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt index 5f6e6a8..686487d 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ChatCompletions.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import co.yml.ychat.domain.model.ChatMessage import kotlin.coroutines.cancellation.CancellationException @@ -68,9 +68,9 @@ interface ChatCompletions { * @param content The content of the user input. * @return A list of chat message objects representing the possible completions. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error generating chat completions. + * @throws YChatException if there is an error generating chat completions. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(content: String): List /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Completion.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Completion.kt index 1de5197..bf9cca7 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Completion.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Completion.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import kotlin.coroutines.cancellation.CancellationException /** @@ -60,7 +60,7 @@ interface Completion { * Execute completion request. * @return one predicted completion of the given prompt. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(): String /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Edits.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Edits.kt index 3785107..d19a7c2 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Edits.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/Edits.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import kotlin.coroutines.cancellation.CancellationException interface Edits { @@ -56,9 +56,9 @@ interface Edits { * @param instruction The instruction to generate edits for. * @return A list of edits generated for the given instruction. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error generating edits. + * @throws YChatException if there is an error generating edits. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(instruction: String): List /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ImageGenerations.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ImageGenerations.kt index 64b9f68..9810215 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ImageGenerations.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ImageGenerations.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import kotlin.coroutines.cancellation.CancellationException interface ImageGenerations { @@ -34,9 +34,9 @@ interface ImageGenerations { * @param prompt The prompt for generating the images. * @return A list of generated image objects. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error generating images. + * @throws YChatException if there is an error generating images. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(prompt: String): List /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ListModels.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ListModels.kt index 89882d0..5bdc90b 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ListModels.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/ListModels.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import co.yml.ychat.domain.model.AIModel import kotlin.coroutines.cancellation.CancellationException @@ -13,9 +13,9 @@ interface ListModels { * * @return A list of artificial intelligence models. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error generating chat completions. + * @throws YChatException if there is an error generating chat completions. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(): List /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/RetrieveModel.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/RetrieveModel.kt index 282bb83..3377982 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/RetrieveModel.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/features/RetrieveModel.kt @@ -1,7 +1,7 @@ package co.yml.ychat.entrypoint.features import co.yml.ychat.YChat -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import co.yml.ychat.domain.model.AIModel import kotlin.coroutines.cancellation.CancellationException @@ -14,9 +14,9 @@ interface RetrieveModel { * @param id The ID of the model to retrieve. * @return The artificial intelligence model with the given [id]. * @throws CancellationException if the operation is cancelled. - * @throws ChatGptException if there is an error generating chat completions. + * @throws YChatException if there is an error generating chat completions. */ - @Throws(CancellationException::class, ChatGptException::class) + @Throws(CancellationException::class, YChatException::class) suspend fun execute(id: String): AIModel /** diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranscriptionsImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranscriptionsImpl.kt index 38e2ab4..c98ee64 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranscriptionsImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranscriptionsImpl.kt @@ -1,8 +1,8 @@ package co.yml.ychat.entrypoint.impl import co.yml.ychat.YChat +import co.yml.ychat.core.model.FileBytes import co.yml.ychat.domain.model.AudioParams -import co.yml.ychat.domain.model.FileBytes import co.yml.ychat.domain.usecases.AudioUseCase import co.yml.ychat.entrypoint.features.AudioTranscriptions import kotlinx.coroutines.CoroutineDispatcher diff --git a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranslationsImpl.kt b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranslationsImpl.kt index 46387e4..b9c1ec4 100644 --- a/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranslationsImpl.kt +++ b/ychat/src/commonMain/kotlin/co/yml/ychat/entrypoint/impl/AudioTranslationsImpl.kt @@ -1,8 +1,8 @@ package co.yml.ychat.entrypoint.impl import co.yml.ychat.YChat +import co.yml.ychat.core.model.FileBytes import co.yml.ychat.domain.model.AudioParams -import co.yml.ychat.domain.model.FileBytes import co.yml.ychat.domain.usecases.AudioUseCase import co.yml.ychat.entrypoint.features.AudioTranslations import kotlinx.coroutines.CoroutineDispatcher diff --git a/ychat/src/iosMain/kotlin/co/yml/ychat/di/module/platformModule.kt b/ychat/src/iosMain/kotlin/co/yml/ychat/di/module/platformModule.kt deleted file mode 100644 index fc3dca7..0000000 --- a/ychat/src/iosMain/kotlin/co/yml/ychat/di/module/platformModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package co.yml.ychat.di.module - -import io.ktor.client.engine.darwin.Darwin -import org.koin.dsl.module - -internal actual fun platformModule() = module { - single { Darwin.create() } -} diff --git a/ychat/src/jvmMain/kotlin/co/yml/ychat/di/module/platformModule.kt b/ychat/src/jvmMain/kotlin/co/yml/ychat/di/module/platformModule.kt deleted file mode 100644 index 57f48fe..0000000 --- a/ychat/src/jvmMain/kotlin/co/yml/ychat/di/module/platformModule.kt +++ /dev/null @@ -1,8 +0,0 @@ -package co.yml.ychat.di.module - -import io.ktor.client.engine.okhttp.OkHttp -import org.koin.dsl.module - -internal actual fun platformModule() = module { - single { OkHttp.create() } -} diff --git a/ychat/src/jvmMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt b/ychat/src/jvmMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt deleted file mode 100644 index a1f9ce5..0000000 --- a/ychat/src/jvmMain/kotlin/co/yml/ychat/domain/model/FileBytes.kt +++ /dev/null @@ -1,7 +0,0 @@ -package co.yml.ychat.domain.model - -actual typealias FileBytes = ByteArray - -internal actual fun FileBytes.toByteArray(): ByteArray { - return this -} From 8a4564995520a26697d7601a605c75064a5efd10 Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Wed, 26 Apr 2023 22:22:35 -0300 Subject: [PATCH 2/4] refactor: Move tests to :ychat-core --- .../infrastructure/ApiExecutorTest.kt | 42 ++++++++++++++++--- .../network}/infrastructure/ApiResultTest.kt | 8 ++-- .../network/infrastructure/MockHttpClient.kt | 11 +++++ .../ychat/core}/storage/ChatLogStorageTest.kt | 2 +- .../infrastructure/OpenAiHttpClientTest.kt} | 7 ++-- .../co/yml/ychat/di/LibraryModuleTest.kt | 8 ++-- .../ychat/domain/usecases/AudioUseCaseTest.kt | 14 +++---- .../usecases/ChatCompletionsUseCaseTest.kt | 8 ++-- .../domain/usecases/CompletionUseCaseTest.kt | 10 ++--- .../ychat/domain/usecases/EditsUseCaseTest.kt | 10 ++--- .../usecases/ImageGenerationsUseCaseTest.kt | 12 +++--- .../domain/usecases/ListModelsUseCaseTest.kt | 8 ++-- .../usecases/RetrieveModelUseCaseTest.kt | 8 ++-- .../co/yml/ychat/entrypoint/YChatTest.kt | 7 ++-- 14 files changed, 99 insertions(+), 56 deletions(-) rename {ychat/src/commonTest/kotlin/co/yml/ychat/data => ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network}/infrastructure/ApiExecutorTest.kt (65%) rename {ychat/src/commonTest/kotlin/co/yml/ychat/data => ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network}/infrastructure/ApiResultTest.kt (85%) create mode 100644 ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/MockHttpClient.kt rename {ychat/src/commonTest/kotlin/co/yml/ychat/data => ychat-core/src/commonTest/kotlin/co/yml/ychat/core}/storage/ChatLogStorageTest.kt (98%) rename ychat/src/commonTest/kotlin/co/yml/ychat/{di/NetworkProviderTest.kt => data/infrastructure/OpenAiHttpClientTest.kt} (84%) diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiExecutorTest.kt b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutorTest.kt similarity index 65% rename from ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiExecutorTest.kt rename to ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutorTest.kt index 922bde9..90d616a 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiExecutorTest.kt +++ b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutorTest.kt @@ -1,6 +1,5 @@ -package co.yml.ychat.data.infrastructure +package co.yml.ychat.core.network.infrastructure -import io.ktor.client.HttpClient import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpHeaders @@ -29,7 +28,7 @@ class ApiExecutorTest { headers = headersOf(HttpHeaders.ContentType, "application/json") ) } - val httpClient = HttpClient(mockEngine) + val httpClient = MockHttpClient(mockEngine) val apiExecutor = ApiExecutor(httpClient) // act @@ -46,6 +45,39 @@ class ApiExecutorTest { assertEquals("This is a test", result.body) } + @Test + fun `on execute when request is multipart type then content type should be multipart type`() = runBlocking { + // arrange + var endpoint = "" + var httpMethod = "" + var contentType: String? = null + val mockEngine = MockEngine { request -> + endpoint = request.url.toString().decodeURLPart() + httpMethod = request.method.value + contentType = request.body.contentType?.contentType + respond( + content = "This is a test", + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + val httpClient = MockHttpClient(mockEngine) + val apiExecutor = ApiExecutor(httpClient) + + // act + apiExecutor + .setEndpoint("api/test") + .setHttpMethod(HttpMethod.Post) + .addFormPart("key1", "value1") + .addFormPart("key1", "file_mock", ByteArray(100)) + .execute() + + // assert + assertEquals("http://localhost/api/test", endpoint) + assertEquals("POST", httpMethod) + assertEquals("multipart", contentType) + } + @Test fun `on execute when occurs api error then should return error as expected`() = runBlocking { // arrange @@ -56,7 +88,7 @@ class ApiExecutorTest { headers = headersOf(HttpHeaders.ContentType, "application/json") ) } - val httpClient = HttpClient(mockEngine) + val httpClient = MockHttpClient(mockEngine) val apiExecutor = ApiExecutor(httpClient) // act @@ -75,7 +107,7 @@ class ApiExecutorTest { val mockEngine = MockEngine { throw IOException("Error") } - val httpClient = HttpClient(mockEngine) + val httpClient = MockHttpClient(mockEngine) val apiExecutor = ApiExecutor(httpClient) // act diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiResultTest.kt b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiResultTest.kt similarity index 85% rename from ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiResultTest.kt rename to ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiResultTest.kt index 151874a..d7fef24 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/ApiResultTest.kt +++ b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/ApiResultTest.kt @@ -1,6 +1,6 @@ -package co.yml.ychat.data.infrastructure +package co.yml.ychat.core.network.infrastructure -import co.yml.ychat.data.exception.ChatGptException +import co.yml.ychat.core.exceptions.YChatException import kotlin.test.Test import kotlin.test.assertEquals @@ -21,7 +21,7 @@ class ApiResultTest { @Test fun `on isSuccessful when has exception then should return false`() { // arrange - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) // act val result = apiResult.isSuccessful @@ -57,7 +57,7 @@ class ApiResultTest { @Test fun `on ensureSuccess when is not successful should throw an exception`() { // arrange - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) // act val result = runCatching { apiResult.ensureSuccess() }.exceptionOrNull() diff --git a/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/MockHttpClient.kt b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/MockHttpClient.kt new file mode 100644 index 0000000..ab66e99 --- /dev/null +++ b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/network/infrastructure/MockHttpClient.kt @@ -0,0 +1,11 @@ +package co.yml.ychat.core.network.infrastructure + +import co.yml.ychat.core.network.factories.HttpClientFactory +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine + +class MockHttpClient(private val mockEngine: MockEngine) : HttpClientFactory { + override fun getHttpClient(): HttpClient { + return HttpClient(mockEngine) + } +} diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/data/storage/ChatLogStorageTest.kt b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/storage/ChatLogStorageTest.kt similarity index 98% rename from ychat/src/commonTest/kotlin/co/yml/ychat/data/storage/ChatLogStorageTest.kt rename to ychat-core/src/commonTest/kotlin/co/yml/ychat/core/storage/ChatLogStorageTest.kt index 7782f6a..7fad7ec 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/data/storage/ChatLogStorageTest.kt +++ b/ychat-core/src/commonTest/kotlin/co/yml/ychat/core/storage/ChatLogStorageTest.kt @@ -1,4 +1,4 @@ -package co.yml.ychat.data.storage +package co.yml.ychat.core.storage import kotlin.test.BeforeTest import kotlin.test.Test diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/di/NetworkProviderTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClientTest.kt similarity index 84% rename from ychat/src/commonTest/kotlin/co/yml/ychat/di/NetworkProviderTest.kt rename to ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClientTest.kt index a3d198e..1bf222a 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/di/NetworkProviderTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/data/infrastructure/OpenAiHttpClientTest.kt @@ -1,6 +1,5 @@ -package co.yml.ychat.di +package co.yml.ychat.data.infrastructure -import co.yml.ychat.di.provider.NetworkProvider import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.client.request.request @@ -12,7 +11,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking -class NetworkProviderTest { +class OpenAiHttpClientTest { @Test fun `on provideHttpClient should assert default request`() { @@ -31,7 +30,7 @@ class NetworkProviderTest { } // act - runBlocking { NetworkProvider.provideHttpClient(mockEngine, apiKey).request() } + runBlocking { OpenAiHttpClient(apiKey, mockEngine).getHttpClient().request() } // assert assertEquals("https://api.openai.com", baseUrl) diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt index fc3a223..97be7ef 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/di/LibraryModuleTest.kt @@ -1,8 +1,9 @@ package co.yml.ychat.di +import co.yml.ychat.core.network.factories.HttpClientFactory +import co.yml.ychat.core.network.infrastructure.ApiExecutor +import co.yml.ychat.core.storage.ChatLogStorage import co.yml.ychat.data.api.ChatGptApi -import co.yml.ychat.data.infrastructure.ApiExecutor -import co.yml.ychat.data.storage.ChatLogStorage import co.yml.ychat.di.module.LibraryModule import co.yml.ychat.domain.usecases.AudioUseCase import co.yml.ychat.domain.usecases.ChatCompletionsUseCase @@ -16,7 +17,6 @@ import co.yml.ychat.entrypoint.features.Completion import co.yml.ychat.entrypoint.features.Edits import co.yml.ychat.entrypoint.features.ImageGenerations import co.yml.ychat.entrypoint.features.ListModels -import io.ktor.client.HttpClient import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test @@ -59,7 +59,7 @@ class LibraryModuleTest : KoinTest { @Test fun `should inject all data modules without throwing exception`() { - get() + get() get() get() get() diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/AudioUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/AudioUseCaseTest.kt index 10d8da1..5ffae17 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/AudioUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/AudioUseCaseTest.kt @@ -1,11 +1,11 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.model.FileBytes +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.AudioResultDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import co.yml.ychat.domain.model.AudioParams -import co.yml.ychat.domain.model.FileBytes import io.mockk.coEvery import io.mockk.mockk import kotlin.test.BeforeTest @@ -47,7 +47,7 @@ class AudioUseCaseTest { val fileName = "audio-test.m4a" val audioFile = ByteArray(1024) as FileBytes val audioParams = AudioParams() - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.audioTranscriptions(any()) } returns apiResult // act @@ -56,7 +56,7 @@ class AudioUseCaseTest { } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } @Test @@ -82,7 +82,7 @@ class AudioUseCaseTest { val fileName = "audio-test.m4a" val audioFile = ByteArray(1024) as FileBytes val audioParams = AudioParams() - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.audioTranslations(any()) } returns apiResult // act @@ -91,6 +91,6 @@ class AudioUseCaseTest { } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } } diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt index 93ac91d..2033536 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ChatCompletionsUseCaseTest.kt @@ -1,12 +1,12 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.ChatCompletionsChoiceDto import co.yml.ychat.data.dto.ChatCompletionsDto import co.yml.ychat.data.dto.ChatMessageDto import co.yml.ychat.data.dto.UsageDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import co.yml.ychat.domain.model.ChatCompletionsParams import co.yml.ychat.domain.model.ChatMessage import io.mockk.coEvery @@ -48,7 +48,7 @@ class ChatCompletionsUseCaseTest { // arrange val messages = arrayListOf(ChatMessage("user", "Say this is a test.")) val params = ChatCompletionsParams(messages) - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.chatCompletions(any()) } returns apiResult // act @@ -56,7 +56,7 @@ class ChatCompletionsUseCaseTest { runCatching { runBlocking { chatCompletionsUseCase.requestChatCompletions(params) } } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } private fun buildChatCompletionsDto(answer: String): ChatCompletionsDto { diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt index 96a7c88..913f4cc 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/CompletionUseCaseTest.kt @@ -1,12 +1,12 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult +import co.yml.ychat.core.storage.ChatLogStorage import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.ChoiceDto import co.yml.ychat.data.dto.CompletionDto import co.yml.ychat.data.dto.UsageDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult -import co.yml.ychat.data.storage.ChatLogStorage import co.yml.ychat.domain.model.CompletionParams import io.mockk.coEvery import io.mockk.every @@ -36,7 +36,7 @@ class CompletionUseCaseTest { fun `on completion when request not succeed then should remove last appended input and throw exception`() { // arrange val input = "Say this is a test." - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) val completionParams = CompletionParams(prompt = input, enableChatStorage = true) every { chatLogStorageMock.buildChatInput(input) } returns input every { chatLogStorageMock.removeLastAppendedInput() } just runs @@ -48,7 +48,7 @@ class CompletionUseCaseTest { // assert verify(exactly = 1) { chatLogStorageMock.removeLastAppendedInput() } - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } @Test diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/EditsUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/EditsUseCaseTest.kt index a447def..e12978f 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/EditsUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/EditsUseCaseTest.kt @@ -1,18 +1,18 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.EditsChoiceDto import co.yml.ychat.data.dto.EditsDto import co.yml.ychat.data.dto.UsageDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import co.yml.ychat.domain.model.EditsParams import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.runBlocking import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking class EditsUseCaseTest { @@ -46,7 +46,7 @@ class EditsUseCaseTest { // arrange val prompt = "text" val params = EditsParams(input = prompt) - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.edits(any()) } returns apiResult // act @@ -54,7 +54,7 @@ class EditsUseCaseTest { runCatching { runBlocking { editsUseCase.requestEdits(params) } } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } private fun buildEditsDto(texts: List): EditsDto { diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ImageGenerationsUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ImageGenerationsUseCaseTest.kt index b9671fd..2c78579 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ImageGenerationsUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ImageGenerationsUseCaseTest.kt @@ -1,17 +1,17 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi -import co.yml.ychat.data.dto.ImageGenerationsDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import co.yml.ychat.data.dto.ImageGeneratedDto +import co.yml.ychat.data.dto.ImageGenerationsDto import co.yml.ychat.domain.model.ImageGenerationsParams import io.mockk.coEvery import io.mockk.mockk -import kotlinx.coroutines.runBlocking import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlinx.coroutines.runBlocking class ImageGenerationsUseCaseTest { @@ -45,7 +45,7 @@ class ImageGenerationsUseCaseTest { // arrange val prompt = "/image test" val params = ImageGenerationsParams(prompt = prompt) - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.imageGenerations(any()) } returns apiResult // act @@ -53,7 +53,7 @@ class ImageGenerationsUseCaseTest { runCatching { runBlocking { imageGenerationsUseCase.requestImageGenerations(params) } } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } private fun buildImageGenerationsDto(url: String): ImageGenerationsDto { diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ListModelsUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ListModelsUseCaseTest.kt index 74d205b..c93de0e 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ListModelsUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/ListModelsUseCaseTest.kt @@ -1,10 +1,10 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.ModelDto import co.yml.ychat.data.dto.ModelListDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import io.mockk.coEvery import io.mockk.mockk import kotlin.test.BeforeTest @@ -42,14 +42,14 @@ class ListModelsUseCaseTest { @Test fun `on getListModels when not request succeed then should throw an exception`() { // arrange - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.models() } returns apiResult // act val result = runCatching { runBlocking { useCase.getListModels() } } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } private fun buildModelListDto(modelIds: List): ModelListDto { diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/RetrieveModelUseCaseTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/RetrieveModelUseCaseTest.kt index d25e8bc..468efa7 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/RetrieveModelUseCaseTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/domain/usecases/RetrieveModelUseCaseTest.kt @@ -1,9 +1,9 @@ package co.yml.ychat.domain.usecases +import co.yml.ychat.core.exceptions.YChatException +import co.yml.ychat.core.network.infrastructure.ApiResult import co.yml.ychat.data.api.ChatGptApi import co.yml.ychat.data.dto.ModelDto -import co.yml.ychat.data.exception.ChatGptException -import co.yml.ychat.data.infrastructure.ApiResult import io.mockk.coEvery import io.mockk.mockk import kotlin.test.BeforeTest @@ -40,14 +40,14 @@ class RetrieveModelUseCaseTest { @Test fun `on getModel when not request succeed then should throw an exception`() { // arrange - val apiResult = ApiResult(exception = ChatGptException()) + val apiResult = ApiResult(exception = YChatException()) coEvery { chatGptApiMock.model("1") } returns apiResult // act val result = runCatching { runBlocking { useCase.getModel("1") } } // assert - assertEquals(true, result.exceptionOrNull() is ChatGptException) + assertEquals(true, result.exceptionOrNull() is YChatException) } private fun buildModelDto(id: String): ModelDto { diff --git a/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt b/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt index b0899dc..1e9ba55 100644 --- a/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt +++ b/ychat/src/commonTest/kotlin/co/yml/ychat/entrypoint/YChatTest.kt @@ -1,10 +1,11 @@ package co.yml.ychat.entrypoint import co.yml.ychat.YChat -import co.yml.ychat.domain.model.FileBytes +import co.yml.ychat.core.model.FileBytes +import co.yml.ychat.core.network.factories.HttpClientFactory +import co.yml.ychat.data.infrastructure.OpenAiHttpClient import co.yml.ychat.entrypoint.impl.YChatImpl import infrastructure.MockStorage -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpHeaders @@ -200,7 +201,7 @@ class YChatTest { ) } val module = module { - single { httpEngine } + single { OpenAiHttpClient("api.key", httpEngine) } } yChat.koinApp.modules(module) } From bb175e6c615fd376dd9f043b392777e14529a938 Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Fri, 28 Apr 2023 10:48:14 -0300 Subject: [PATCH 3/4] chore: add explicitApi mode in the core internal library --- ychat-core/build.gradle.kts | 1 + .../co/yml/ychat/core/model/FileBytes.kt | 4 ++-- .../network/factories/HttpEngineFactory.kt | 4 ++-- .../ychat/core/exceptions/YChatException.kt | 6 ++--- .../co/yml/ychat/core/model/FileBytes.kt | 4 ++-- .../network/extensions/ApiResultExtensions.kt | 6 ++--- .../network/factories/HttpClientFactory.kt | 4 ++-- .../network/factories/HttpEngineFactory.kt | 4 ++-- .../network/infrastructure/ApiExecutor.kt | 24 +++++++++---------- .../core/network/infrastructure/ApiResult.kt | 6 ++--- .../yml/ychat/core/storage/ChatLogStorage.kt | 10 ++++---- .../co/yml/ychat/core/model/FileBytes.kt | 4 ++-- .../network/factories/HttpEngineFactory.kt | 4 ++-- .../co/yml/ychat/core/model/FileBytes.kt | 4 ++-- .../network/factories/HttpEngineFactory.kt | 4 ++-- 15 files changed, 45 insertions(+), 44 deletions(-) diff --git a/ychat-core/build.gradle.kts b/ychat-core/build.gradle.kts index f3d84af..ceceb72 100644 --- a/ychat-core/build.gradle.kts +++ b/ychat-core/build.gradle.kts @@ -18,6 +18,7 @@ kover { } kotlin { + explicitApi() android() jvm() listOf( diff --git a/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index d75fd32..bb733ed 100644 --- a/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt +++ b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -1,7 +1,7 @@ package co.yml.ychat.core.model -actual typealias FileBytes = ByteArray +public actual typealias FileBytes = ByteArray -actual fun FileBytes.toByteArray(): ByteArray { +public actual fun FileBytes.toByteArray(): ByteArray { return this } diff --git a/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt index c9d1360..f9ed64e 100644 --- a/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt +++ b/ychat-core/src/androidMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -3,8 +3,8 @@ package co.yml.ychat.core.network.factories import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp -actual object HttpEngineFactory { - actual fun getEngine(): HttpClientEngine { +public actual object HttpEngineFactory { + public actual fun getEngine(): HttpClientEngine { return OkHttp.create() } } diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt index ad03b96..6081dad 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/exceptions/YChatException.kt @@ -1,12 +1,12 @@ package co.yml.ychat.core.exceptions -class YChatException( +public class YChatException( message: String? = null, cause: Throwable? = null, - var statusCode: Int? = null, + public var statusCode: Int? = null, ) : Exception(message, cause) { - constructor( + public constructor( cause: Throwable?, statusCode: Int? = null ) : this(message = null, cause = cause, statusCode = statusCode) diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index 83960ca..999fe2f 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -1,5 +1,5 @@ package co.yml.ychat.core.model -expect class FileBytes +public expect class FileBytes -expect fun FileBytes.toByteArray(): ByteArray +public expect fun FileBytes.toByteArray(): ByteArray diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt index 018505c..06aa36d 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/extensions/ApiResultExtensions.kt @@ -9,7 +9,7 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.util.toMap -suspend inline fun HttpResponse.toApiResult(): ApiResult { +public suspend inline fun HttpResponse.toApiResult(): ApiResult { val headers = this.headers.toMap() val statusCode = this.status.value return if (!this.status.isSuccess()) { @@ -21,14 +21,14 @@ suspend inline fun HttpResponse.toApiResult(): ApiResult { } } -fun ResponseException.toApiResult(): ApiResult { +public fun ResponseException.toApiResult(): ApiResult { return ApiResult( statusCode = this.response.status.value, exception = YChatException(this.cause, this.response.status.value) ) } -fun Throwable.toApiResult(): ApiResult { +public fun Throwable.toApiResult(): ApiResult { return ApiResult( statusCode = null, exception = YChatException(this.cause) diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt index 104b706..60c8d50 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpClientFactory.kt @@ -2,6 +2,6 @@ package co.yml.ychat.core.network.factories import io.ktor.client.HttpClient -interface HttpClientFactory { - fun getHttpClient(): HttpClient +public interface HttpClientFactory { + public fun getHttpClient(): HttpClient } diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt index f82d35c..aa62500 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -2,6 +2,6 @@ package co.yml.ychat.core.network.factories import io.ktor.client.engine.HttpClientEngine -expect object HttpEngineFactory { - actual fun getEngine(): HttpClientEngine +public expect object HttpEngineFactory { + public actual fun getEngine(): HttpClientEngine } diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt index ebeb448..6469a60 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiExecutor.kt @@ -17,7 +17,7 @@ import io.ktor.http.HttpMethod import io.ktor.utils.io.errors.IOException import kotlin.collections.set -class ApiExecutor(private val httpClientFactory: HttpClientFactory) { +public class ApiExecutor(private val httpClientFactory: HttpClientFactory) { private val httpClient by lazy { httpClientFactory.getHttpClient() } @@ -29,39 +29,39 @@ class ApiExecutor(private val httpClientFactory: HttpClientFactory) { private var query: HashMap = HashMap() - val formParts = mutableListOf>() + public val formParts: MutableList> = mutableListOf() - fun setEndpoint(endpoint: String): ApiExecutor { + public fun setEndpoint(endpoint: String): ApiExecutor { this.endpoint = endpoint return this } - fun setHttpMethod(httpMethod: HttpMethod): ApiExecutor { + public fun setHttpMethod(httpMethod: HttpMethod): ApiExecutor { this.httpMethod = httpMethod return this } - fun setBody(body: Any): ApiExecutor { + public fun setBody(body: Any): ApiExecutor { this.body = body return this } - fun addQuery(key: String, value: String): ApiExecutor { + public fun addQuery(key: String, value: String): ApiExecutor { this.query[key] = value return this } - fun addQuery(key: String, value: List): ApiExecutor { + public fun addQuery(key: String, value: List): ApiExecutor { this.query[key] = value.joinToString(",") return this } - fun addFormPart(key: String, value: T): ApiExecutor { + public fun addFormPart(key: String, value: T): ApiExecutor { formParts += FormPart(key, value) return this } - fun addFormPart(key: String, fileName: String, value: ByteArray): ApiExecutor { + public fun addFormPart(key: String, fileName: String, value: ByteArray): ApiExecutor { val headers = Headers.build { append(HttpHeaders.ContentType, ContentType.Application.OctetStream.contentType) append(HttpHeaders.ContentDisposition, "filename=$fileName") @@ -70,7 +70,7 @@ class ApiExecutor(private val httpClientFactory: HttpClientFactory) { return this } - suspend inline fun execute(): ApiResult { + public suspend inline fun execute(): ApiResult { return try { val response = if (formParts.isEmpty()) executeRequest() else executeRequestAsForm() return response.toApiResult() @@ -81,7 +81,7 @@ class ApiExecutor(private val httpClientFactory: HttpClientFactory) { } } - suspend fun executeRequest(): HttpResponse { + public suspend fun executeRequest(): HttpResponse { return httpClient.request(endpoint) { url { query.forEach { parameters.append(it.key, it.value) } } method = httpMethod @@ -89,7 +89,7 @@ class ApiExecutor(private val httpClientFactory: HttpClientFactory) { } } - suspend fun executeRequestAsForm(): HttpResponse { + public suspend fun executeRequestAsForm(): HttpResponse { return httpClient.submitFormWithBinaryData( url = endpoint, formData = formData { formParts.forEach { append(it) } } diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt index d8506d9..b93c023 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/network/infrastructure/ApiResult.kt @@ -3,7 +3,7 @@ package co.yml.ychat.core.network.infrastructure import co.yml.ychat.core.exceptions.YChatException /** Encapsulates an outcome from source api. */ -data class ApiResult( +public data class ApiResult( val body: T? = null, val headers: Map> = mapOf(), val statusCode: Int? = null, @@ -14,14 +14,14 @@ data class ApiResult( val isSuccessful: Boolean = exception == null /** Try to get [body], if it is null an [YChatException] will be thrown. */ - fun getBodyOrThrow(): T { + public fun getBodyOrThrow(): T { val exception = exception ?: YChatException("Could not retrieve body from ApiResult.") return body ?: throw exception } /** Throw an [exception] when [isSuccessful] is false. */ - fun ensureSuccess() { + public fun ensureSuccess() { if (!isSuccessful) throw exception ?: YChatException("Api request was not successful.") } diff --git a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt index 381c9d8..5c0e8a5 100644 --- a/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt +++ b/ychat-core/src/commonMain/kotlin/co/yml/ychat/core/storage/ChatLogStorage.kt @@ -1,23 +1,23 @@ package co.yml.ychat.core.storage -class ChatLogStorage { +public class ChatLogStorage { private val chatLog: MutableList = mutableListOf() - fun getChatLog(): String { + public fun getChatLog(): String { return chatLog.joinToString("\n") } - fun buildChatInput(input: String): String { + public fun buildChatInput(input: String): String { chatLog.add("Human: $input") return getChatLog() + "\n" + "AI: " } - fun removeLastAppendedInput() { + public fun removeLastAppendedInput() { chatLog.removeLast() } - fun appendAnswer(answer: String) { + public fun appendAnswer(answer: String) { chatLog.add("AI: $answer") } } diff --git a/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index 28a4e82..3044c9a 100644 --- a/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt +++ b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -5,9 +5,9 @@ import kotlinx.cinterop.usePinned import platform.Foundation.NSData import platform.posix.memcpy -actual typealias FileBytes = NSData +public actual typealias FileBytes = NSData -actual fun FileBytes.toByteArray(): ByteArray { +public actual fun FileBytes.toByteArray(): ByteArray { return ByteArray(this.length.toInt()).apply { usePinned { memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) diff --git a/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt index 4f9c4c8..d48cb96 100644 --- a/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt +++ b/ychat-core/src/iosMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -3,9 +3,9 @@ package co.yml.ychat.core.network.factories import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.darwin.Darwin -actual object HttpEngineFactory { +public actual object HttpEngineFactory { - actual fun getEngine(): HttpClientEngine { + public actual fun getEngine(): HttpClientEngine { return Darwin.create() } } diff --git a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt index d75fd32..bb733ed 100644 --- a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt +++ b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/model/FileBytes.kt @@ -1,7 +1,7 @@ package co.yml.ychat.core.model -actual typealias FileBytes = ByteArray +public actual typealias FileBytes = ByteArray -actual fun FileBytes.toByteArray(): ByteArray { +public actual fun FileBytes.toByteArray(): ByteArray { return this } diff --git a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt index 73b4646..94c6945 100644 --- a/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt +++ b/ychat-core/src/jvmMain/kotlin/co/yml/ychat/core/network/factories/HttpEngineFactory.kt @@ -3,9 +3,9 @@ package co.yml.ychat.core.network.factories import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.okhttp.OkHttp -actual object HttpEngineFactory { +public actual object HttpEngineFactory { - actual fun getEngine(): HttpClientEngine { + public actual fun getEngine(): HttpClientEngine { return OkHttp.create() } } From d3dd11fec17f5fda99a142bf642ab591a368e358 Mon Sep 17 00:00:00 2001 From: Koji Osugi Date: Mon, 1 May 2023 22:41:04 -0300 Subject: [PATCH 4/4] chore: add ios framework basename --- ychat-core/build.gradle.kts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ychat-core/build.gradle.kts b/ychat-core/build.gradle.kts index ceceb72..8aeed13 100644 --- a/ychat-core/build.gradle.kts +++ b/ychat-core/build.gradle.kts @@ -25,7 +25,11 @@ kotlin { iosX64(), iosArm64(), iosSimulatorArm64() - ) + ).forEach { + it.binaries.framework { + baseName = "YChatCore" + } + } sourceSets { val commonMain by getting {