Skip to content

Commit

Permalink
Add error handling for API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
StefMa committed Nov 8, 2023
1 parent 3677072 commit 1d4ef97
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 31 deletions.
31 changes: 18 additions & 13 deletions src/commonMain/kotlin/com/ioki/lokalise/api/LokaliseClient.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ioki.lokalise.api

import com.ioki.lokalise.api.models.ErrorWrapper
import com.ioki.lokalise.api.models.FileDownload
import com.ioki.lokalise.api.models.FileUpload
import com.ioki.lokalise.api.models.Projects
Expand All @@ -16,6 +17,7 @@ import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
Expand All @@ -39,7 +41,7 @@ interface LokaliseProjects {
*/
suspend fun allProjects(
queryParams: Map<String, Any> = emptyMap()
): Projects
): Result<Projects>
}

interface LokaliseFiles {
Expand All @@ -52,7 +54,7 @@ interface LokaliseFiles {
projectId: String,
format: String,
bodyParams: Map<String, Any> = emptyMap()
): FileDownload
): Result<FileDownload>

/**
* Upload files.
Expand All @@ -64,7 +66,7 @@ interface LokaliseFiles {
filename: String,
langIso: String,
bodyParams: Map<String, Any> = emptyMap()
): FileUpload
): Result<FileUpload>
}

interface LokaliseQueuedProcesses {
Expand All @@ -76,7 +78,7 @@ interface LokaliseQueuedProcesses {
suspend fun retrieveProcess(
projectId: String,
processId: String,
): RetrievedProcess
): Result<RetrievedProcess>
}

/**
Expand All @@ -95,7 +97,6 @@ internal fun Lokalise(
fullLoggingEnabled: Boolean = false,
httpClientEngine: HttpClientEngine? = null
): Lokalise {

val clientConfig: HttpClientConfig<*>.() -> Unit = {
install(ContentNegotiation) {
json()
Expand All @@ -122,29 +123,29 @@ private class LokaliseClient(
private val httpClient: HttpClient,
) : Lokalise {

override suspend fun allProjects(queryParams: Map<String, Any>): Projects {
override suspend fun allProjects(queryParams: Map<String, Any>): Result<Projects> {
val requestParams = queryParams
.map { "${it.key}=${it.value}" }
.joinToString(separator = "&")
.run { if (isNotBlank()) "?$this" else this }

return httpClient
.get("projects$requestParams")
.body()
.toResult()
}

override suspend fun downloadFiles(
projectId: String,
format: String,
bodyParams: Map<String, Any>
): FileDownload {
): Result<FileDownload> {
val requestBody = bodyParams.toMutableMap()
.apply { put("format", format) }
.toRequestBody()

return httpClient
.post("projects/$projectId/files/download") { setBody(requestBody) }
.body()
.toResult()
}

override suspend fun uploadFile(
Expand All @@ -153,7 +154,7 @@ private class LokaliseClient(
filename: String,
langIso: String,
bodyParams: Map<String, Any>
): FileUpload {
): Result<FileUpload> {
val requestBody = bodyParams.toMutableMap()
.apply {
put("data", data)
Expand All @@ -164,19 +165,23 @@ private class LokaliseClient(

return httpClient
.post("projects/$projectId/files/upload") { setBody(requestBody) }
.body()
.toResult()
}

override suspend fun retrieveProcess(
projectId: String,
processId: String
): RetrievedProcess {
): Result<RetrievedProcess> {
return httpClient
.get("projects/$projectId/processes/$processId")
.body()
.toResult()
}
}

private suspend inline fun <reified T> HttpResponse.toResult(): Result<T> =
if (status.value in 200..299) Result.Success(body<T>())
else Result.Failure(body<ErrorWrapper>().error)

/**
* Found at
* [kotlinx.serialization/issues#746](https://github.com/Kotlin/kotlinx.serialization/issues/746#issuecomment-863099397)
Expand Down
8 changes: 8 additions & 0 deletions src/commonMain/kotlin/com/ioki/lokalise/api/Result.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ioki.lokalise.api

import com.ioki.lokalise.api.models.Error

sealed interface Result<out T> {
data class Success<T>(val data: T) : Result<T>
data class Failure(val error: Error) : Result<Nothing>
}
15 changes: 15 additions & 0 deletions src/commonMain/kotlin/com/ioki/lokalise/api/models/Error.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.ioki.lokalise.api.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class ErrorWrapper(
val error: Error,
)

@Serializable
data class Error(
@SerialName("message") val message: String,
@SerialName("code") val code: Int
)
78 changes: 60 additions & 18 deletions src/commonTest/kotlin/com/ioki/lokalise/api/LokaliseClientTest.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.ioki.lokalise.api

import com.ioki.lokalise.api.stubs.projectsJson
import com.ioki.lokalise.api.stubs.errorJson
import com.ioki.lokalise.api.stubs.fileDownloadJson
import com.ioki.lokalise.api.stubs.fileUploadJson
import com.ioki.lokalise.api.stubs.projectsJson
import com.ioki.lokalise.api.stubs.retrieveProcessJson
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.mock.MockEngine
Expand All @@ -11,6 +12,7 @@ import io.ktor.client.engine.mock.toByteArray
import io.ktor.http.ContentType
import io.ktor.http.Headers
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.headersOf
import io.ktor.utils.io.core.String
import kotlinx.coroutines.runBlocking
Expand All @@ -20,6 +22,41 @@ import kotlin.test.assertTrue

class LokaliseClientTest {

@Test
fun `test result value with error on allProjects but doesn't matter where`() = runLokaliseTest(
errorJson,
HttpStatusCode.NotFound
) { lokalise, mockEngine ->
val result = lokalise.allProjects()

assertTrue(result is Result.Failure)
assertTrue(result.error.code == 404)
assertTrue(result.error.message == "Not Found")
}

@Test
fun `test result value without error on allProjects but doesn't matter where`() =
runLokaliseTest(projectsJson) { lokalise, mockEngine ->
val result = lokalise.allProjects()

assertTrue(result is Result.Success)
assertTrue(result.data.projects.size == 1)
with(result.data.projects.first()) {
assertEquals(
expected = projectId,
actual = "string"
)
assertEquals(
expected = createdAtTimestamp,
actual = 0
)
assertEquals(
expected = settings.branching,
actual = true
)
}
}

@Test
fun `test list all projects without params`() = runLokaliseTest(projectsJson) { lokalise, mockEngine ->
lokalise.allProjects()
Expand Down Expand Up @@ -211,28 +248,33 @@ class LokaliseClientTest {
headers.contains("X-Api-Token", "sec3tT0k3n")
}

private fun createLokalise(
httpClientEngine: HttpClientEngine,
token: String = "sec3tT0k3n",
): Lokalise = Lokalise(
token = token,
fullLoggingEnabled = true,
httpClientEngine = httpClientEngine
)

private fun createMockEngine(content: String): MockEngine = MockEngine { _ ->
respond(
content = content,
headers = headersOf("Content-Type", ContentType.Application.Json.toString())
)
}

private fun runLokaliseTest(
httpJsonResponse: String,
httpStatusCode: HttpStatusCode = HttpStatusCode.OK,
block: suspend (Lokalise, MockEngine) -> Unit
) = runBlocking {
val engine = createMockEngine(httpJsonResponse)
val engine = createMockEngine(httpJsonResponse, httpStatusCode)
val lokalise = createLokalise(engine)
block(lokalise, engine)
}
}

private fun createMockEngine(
content: String,
statusCode: HttpStatusCode
): MockEngine = MockEngine { _ ->
respond(
content = content,
headers = headersOf("Content-Type", ContentType.Application.Json.toString()),
status = statusCode
)
}

private fun createLokalise(
httpClientEngine: HttpClientEngine,
token: String = "sec3tT0k3n",
): Lokalise = Lokalise(
token = token,
fullLoggingEnabled = true,
httpClientEngine = httpClientEngine
)
10 changes: 10 additions & 0 deletions src/commonTest/kotlin/com/ioki/lokalise/api/stubs/ErrorJson.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ioki.lokalise.api.stubs

val errorJson = """
{
"error":{
"message": "Not Found",
"code": 404
}
}
""".trimIndent()

0 comments on commit 1d4ef97

Please sign in to comment.