From cb111e07a4bcdd08a6d1eed9b5c84913a71bd1a3 Mon Sep 17 00:00:00 2001 From: Sargun Vohra Date: Fri, 6 Jun 2025 22:43:49 -0700 Subject: [PATCH] handle exceptions --- justfile | 8 +++-- .../kotlin/dev/sargunv/pokekotlin/PokeApi.kt | 5 ++- .../dev/sargunv/pokekotlin/PokeApiError.kt | 3 -- .../sargunv/pokekotlin/PokeApiException.kt | 13 ++++++++ .../pokekotlin/internal/PokeApiConverter.kt | 33 +++++++++++++++++++ .../dev/sargunv/pokekotlin/test/ErrorTest.kt | 24 -------------- .../dev/sargunv/pokekotlin/test/LiveTest.kt | 13 ++++++-- .../pokekotlin/test/PokeApiExceptionTest.kt | 24 ++++++++++++++ 8 files changed, 91 insertions(+), 32 deletions(-) delete mode 100644 src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiError.kt create mode 100644 src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiException.kt create mode 100644 src/commonMain/kotlin/dev/sargunv/pokekotlin/internal/PokeApiConverter.kt delete mode 100644 src/commonTest/kotlin/dev/sargunv/pokekotlin/test/ErrorTest.kt create mode 100644 src/commonTest/kotlin/dev/sargunv/pokekotlin/test/PokeApiExceptionTest.kt diff --git a/justfile b/justfile index e4e64844..d925bf83 100644 --- a/justfile +++ b/justfile @@ -3,8 +3,12 @@ set windows-shell := ["C:\\Program Files\\Git\\bin\\sh.exe", "-c"] _default: just --list +test-server-cmd := "npx -y http-server src/commonTest/resources/data -e json -p 8080 --cors" + +# Serve PokeAPI data from static files test-server: - npx -y http-server src/commonTest/resources/data -e json -p 8080 --cors + {{ test-server-cmd }} +# Spawn a background job serving PokeAPI data from static files test-server-background: - npx -y http-server src/commonTest/resources/data -e json -p 8080 --cors & + {{ test-server-cmd }} & diff --git a/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApi.kt b/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApi.kt index 004afd11..f991f6b0 100644 --- a/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApi.kt +++ b/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApi.kt @@ -6,6 +6,7 @@ import de.jensklingenberg.ktorfit.Ktorfit.Builder import de.jensklingenberg.ktorfit.http.GET import de.jensklingenberg.ktorfit.http.Path import de.jensklingenberg.ktorfit.http.Query +import dev.sargunv.pokekotlin.internal.PokeApiConverter import dev.sargunv.pokekotlin.internal.PokeApiJson import dev.sargunv.pokekotlin.internal.getDefaultEngine import dev.sargunv.pokekotlin.model.Ability @@ -65,6 +66,7 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.cache.storage.CacheStorage import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.http.ContentType import io.ktor.serialization.kotlinx.json.json import kotlin.jvm.JvmName @@ -574,8 +576,9 @@ fun PokeApi( HttpClient(engine) { configure() install(HttpCache) { cacheStorage?.let { privateStorage(it) } } - install(ContentNegotiation) { json(PokeApiJson) } + install(ContentNegotiation) { json(PokeApiJson, ContentType.Any) } } ) + .converterFactories(PokeApiConverter.Factory) .build() .createPokeApi() diff --git a/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiError.kt b/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiError.kt deleted file mode 100644 index 9f83ef58..00000000 --- a/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiError.kt +++ /dev/null @@ -1,3 +0,0 @@ -package dev.sargunv.pokekotlin - -class PokeApiError(val code: Int, message: String) : Throwable("($code) $message") diff --git a/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiException.kt b/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiException.kt new file mode 100644 index 00000000..a47b67eb --- /dev/null +++ b/src/commonMain/kotlin/dev/sargunv/pokekotlin/PokeApiException.kt @@ -0,0 +1,13 @@ +package dev.sargunv.pokekotlin + +import io.ktor.http.HttpStatusCode + +sealed class PokeApiException : Throwable { + constructor(message: String) : super(message) + + constructor(cause: Throwable) : super(cause) + + class HttpStatus(val status: HttpStatusCode) : PokeApiException(status.toString()) + + class UnknownException(cause: Throwable) : PokeApiException(cause) +} diff --git a/src/commonMain/kotlin/dev/sargunv/pokekotlin/internal/PokeApiConverter.kt b/src/commonMain/kotlin/dev/sargunv/pokekotlin/internal/PokeApiConverter.kt new file mode 100644 index 00000000..33cf0dcb --- /dev/null +++ b/src/commonMain/kotlin/dev/sargunv/pokekotlin/internal/PokeApiConverter.kt @@ -0,0 +1,33 @@ +package dev.sargunv.pokekotlin.internal + +import de.jensklingenberg.ktorfit.Ktorfit +import de.jensklingenberg.ktorfit.converter.Converter +import de.jensklingenberg.ktorfit.converter.KtorfitResult +import de.jensklingenberg.ktorfit.converter.TypeData +import dev.sargunv.pokekotlin.PokeApiException +import io.ktor.client.call.body +import io.ktor.client.statement.HttpResponse +import io.ktor.http.isSuccess + +internal class PokeApiConverter(private val typeData: TypeData) : + Converter.SuspendResponseConverter { + override suspend fun convert(result: KtorfitResult): Any { + return when (result) { + is KtorfitResult.Failure -> throw PokeApiException.UnknownException(result.throwable) + is KtorfitResult.Success -> { + val status = result.response.status + when { + status.isSuccess() -> result.response.body(typeData.typeInfo) + else -> throw PokeApiException.HttpStatus(status) + } + } + } + } + + internal object Factory : Converter.Factory { + override fun suspendResponseConverter( + typeData: TypeData, + ktorfit: Ktorfit, + ): Converter.SuspendResponseConverter = PokeApiConverter(typeData) + } +} diff --git a/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/ErrorTest.kt b/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/ErrorTest.kt deleted file mode 100644 index c19d7bbb..00000000 --- a/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/ErrorTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.sargunv.pokekotlin.test - -import dev.sargunv.pokekotlin.PokeApi -import dev.sargunv.pokekotlin.PokeApiError -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlinx.coroutines.test.runTest - -class ErrorTest { - @Test - fun notFoundError() = runTest { - val e = assertFailsWith(PokeApiError::class, "404 Not Found") { LocalPokeApi.getMove(-1) } - assertEquals(404, e.code) - } - - @Test - fun badUrlError() = runTest { - val e = - assertFailsWith(PokeApiError::class, "????") { - PokeApi("https://localhost:12345/").getBerry(10) - } - } -} diff --git a/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/LiveTest.kt b/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/LiveTest.kt index fe64df22..29b4389f 100644 --- a/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/LiveTest.kt +++ b/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/LiveTest.kt @@ -1,18 +1,27 @@ package dev.sargunv.pokekotlin.test import dev.sargunv.pokekotlin.PokeApi +import dev.sargunv.pokekotlin.PokeApiException +import io.ktor.http.HttpStatusCode.Companion.NotFound import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlinx.coroutines.test.runTest @Ignore class LiveTest { - @Test fun liveObject() = runTest { assertEquals("sitrus", PokeApi.getBerry(10).name) } + @Test fun resource() = runTest { assertEquals("sitrus", PokeApi.getBerry(10).name) } @Test - fun liveList() = runTest { + fun list() = runTest { assertEquals(PokeApi.getMoveList(0, 50).results[25], PokeApi.getMoveList(25, 50).results[0]) } + + @Test + fun notFound() = runTest { + val e = assertFailsWith(PokeApiException.HttpStatus::class) { PokeApi.getMove(-1) } + assertEquals(NotFound, e.status) + } } diff --git a/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/PokeApiExceptionTest.kt b/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/PokeApiExceptionTest.kt new file mode 100644 index 00000000..f5014f96 --- /dev/null +++ b/src/commonTest/kotlin/dev/sargunv/pokekotlin/test/PokeApiExceptionTest.kt @@ -0,0 +1,24 @@ +package dev.sargunv.pokekotlin.test + +import dev.sargunv.pokekotlin.PokeApi +import dev.sargunv.pokekotlin.PokeApiException +import io.ktor.http.HttpStatusCode.Companion.NotFound +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlinx.coroutines.test.runTest + +class PokeApiExceptionTest { + @Test + fun notFoundError() = runTest { + val e = assertFailsWith(PokeApiException.HttpStatus::class) { LocalPokeApi.getMove(-1) } + assertEquals(NotFound, e.status) + } + + @Test + fun badUrlError() = runTest { + assertFailsWith(PokeApiException.UnknownException::class) { + PokeApi("https://localhost:12345/").getBerry(10) + } + } +}