Skip to content

Commit

Permalink
Merge pull request #69 from yml-org/refactor/CM-1418/create-core
Browse files Browse the repository at this point in the history
refactor: create core module
  • Loading branch information
osugikoji authored May 2, 2023
2 parents 032cfdb + d3dd11f commit 5f74aa9
Show file tree
Hide file tree
Showing 57 changed files with 412 additions and 257 deletions.
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ dependencyResolutionManagement {
rootProject.name = "ychat-sdk"
include(":ychat")
include(":sample:android")
include(":sample:jvm")
include(":sample:jvm")
include(":ychat-core")
92 changes: 92 additions & 0 deletions ychat-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 {
explicitApi()
android()
jvm()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "YChatCore"
}
}

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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.yml.ychat.core.model

public actual typealias FileBytes = ByteArray

public actual fun FileBytes.toByteArray(): ByteArray {
return this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package co.yml.ychat.core.network.factories

import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp

public actual object HttpEngineFactory {
public actual fun getEngine(): HttpClientEngine {
return OkHttp.create()
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package co.yml.ychat.data.exception
package co.yml.ychat.core.exceptions

class ChatGptException(
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.yml.ychat.core.model

public expect class FileBytes

public expect fun FileBytes.toByteArray(): ByteArray
Original file line number Diff line number Diff line change
@@ -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

public suspend inline fun <reified T> HttpResponse.toApiResult(): ApiResult<T> {
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<T>(), headers, statusCode, null)
}
}

public fun <T> ResponseException.toApiResult(): ApiResult<T> {
return ApiResult(
statusCode = this.response.status.value,
exception = YChatException(this.cause, this.response.status.value)
)
}

public fun <T> Throwable.toApiResult(): ApiResult<T> {
return ApiResult(
statusCode = null,
exception = YChatException(this.cause)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.yml.ychat.core.network.factories

import io.ktor.client.HttpClient

public interface HttpClientFactory {
public fun getHttpClient(): HttpClient
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.yml.ychat.core.network.factories

import io.ktor.client.engine.HttpClientEngine

public expect object HttpEngineFactory {
public actual fun getEngine(): HttpClientEngine
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
public class ApiExecutor(private val httpClientFactory: HttpClientFactory) {

private val httpClient by lazy { httpClientFactory.getHttpClient() }

private lateinit var endpoint: String

Expand All @@ -26,39 +29,39 @@ internal class ApiExecutor(private val httpClient: HttpClient) {

private var query: HashMap<String, String> = HashMap()

private val formParts = mutableListOf<FormPart<*>>()
public val formParts: MutableList<FormPart<*>> = 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<String>): ApiExecutor {
public fun addQuery(key: String, value: List<String>): ApiExecutor {
this.query[key] = value.joinToString(",")
return this
}

fun <T : Any> addFormPart(key: String, value: T): ApiExecutor {
public fun <T : Any> 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")
Expand All @@ -67,7 +70,7 @@ internal class ApiExecutor(private val httpClient: HttpClient) {
return this
}

suspend inline fun <reified T> execute(): ApiResult<T> {
public suspend inline fun <reified T> execute(): ApiResult<T> {
return try {
val response = if (formParts.isEmpty()) executeRequest() else executeRequestAsForm()
return response.toApiResult()
Expand All @@ -78,15 +81,15 @@ internal class ApiExecutor(private val httpClient: HttpClient) {
}
}

private suspend fun executeRequest(): HttpResponse {
public 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 {
public suspend fun executeRequestAsForm(): HttpResponse {
return httpClient.submitFormWithBinaryData(
url = endpoint,
formData = formData { formParts.forEach { append(it) } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package co.yml.ychat.core.network.infrastructure

import co.yml.ychat.core.exceptions.YChatException

/** Encapsulates an outcome from source api. */
public data class ApiResult<T>(
val body: T? = null,
val headers: Map<String, List<String>> = mapOf(),
val statusCode: Int? = 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 [YChatException] will be thrown. */
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. */
public fun ensureSuccess() {
if (!isSuccessful)
throw exception ?: YChatException("Api request was not successful.")
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package co.yml.ychat.data.storage
package co.yml.ychat.core.storage

internal class ChatLogStorage {
public class ChatLogStorage {

private val chatLog: MutableList<String> = 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")
}
}
Loading

0 comments on commit 5f74aa9

Please sign in to comment.