Skip to content

Commit

Permalink
Improve converter error handling #389
Browse files Browse the repository at this point in the history
  • Loading branch information
Foso authored Oct 8, 2023
1 parent b000641 commit 9b90863
Show file tree
Hide file tree
Showing 24 changed files with 300 additions and 130 deletions.
9 changes: 1 addition & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,7 @@ inspired by [Retrofit](https://square.github.io/retrofit/)
Please see the documentation at [https://foso.github.io/Ktorfit/](https://foso.github.io/Ktorfit/)

## Compatibility

| Version | Kotlin | KSP | Ktor |
|--------------------------|:----------------:|:-----------------------:|:---------:|
| **_1.7.0-1.9.20-Beta2_** | **1.9.20-Beta2** | **1.9.20-Beta2-1.0.13** | **2.3.4** |
| **_1.7.0_** | **1.9.10** | **1.0.13** | **2.3.4** |
| **_1.6.0_** | **1.9.10** | **1.0.13** | **2.3.3** |
| **_1.5.0_** | **1.9.0** | **1.0.13** | **2.3.2** |
| **_1.4.3_** | **1.8.20** | **1.0.11** | **2.3.1** |
See https://foso.github.io/Ktorfit/#compatibility

# Release

Expand Down
8 changes: 7 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
All important changes of this project must be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
and this project orients towards [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
Note: This project needs KSP to work and every new Ktorfit with an update of the KSP version is technically a breaking change.
But there is no intent to bump the Ktorfit major version for every KSP update.

Unreleased
========================================
Expand All @@ -15,8 +17,12 @@ Unreleased
- Use @Path parameter name as default value #426
- Use @Query parameter name as default value #428
- Use @Field parameter name as default value #430
- You can now also get exceptions like NetworkException with SuspendResponseConverter. #389

### Deprecated
- Deprecated the `convert` function in the `SuspendResponseConverter` interface
See: https://foso.github.io/migration/#from-170-to-180

### Removed
### Fixed
### Security
Expand Down
30 changes: 30 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Migration
Here is how to migrate from deprecated code:

## From 1.7.0 to 1.8.0

### SuspendResponseConverter
Implement **override suspend fun convert(result: KtorfitResult)**

```kotlin
public suspend fun convert(result: KtorfitResult): T {
return when (result) {
is KtorfitResult.Failure -> {
throw result.throwable // Or do something with the throwable
}

is KtorfitResult.Success -> {
val response = result.response
//Put the code that was in your other convert function here
}
}
}
```

Redirect the deprecated function to the new function:

```kotlin
override suspend fun convert(response: HttpResponse): Any {
return convert(KtorfitResult.Success(response))
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import de.jensklingenberg.ktorfit.Call
import de.jensklingenberg.ktorfit.Callback
import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.converter.Converter
import de.jensklingenberg.ktorfit.converter.KtorfitResult
import de.jensklingenberg.ktorfit.internal.TypeData
import io.ktor.client.call.*
import io.ktor.client.statement.*
Expand Down Expand Up @@ -42,18 +43,30 @@ public class CallConverterFactory : Converter.Factory {
private class CallSuspendResponseConverter(val typeData: TypeData, val ktorfit: Ktorfit) :
Converter.SuspendResponseConverter<HttpResponse, Call<Any?>> {
override suspend fun convert(response: HttpResponse): Call<Any?> {
return convert(KtorfitResult.Success(response))
}

override suspend fun convert(result: KtorfitResult): Call<Any?> {
return object : Call<Any?> {
override fun onExecute(callBack: Callback<Any?>) {
ktorfit.httpClient.launch {
try {
val data = ktorfit.nextSuspendResponseConverter(
null,
typeData.typeArgs.first()
)?.convert(response)
callBack.onResponse(data!!, response)
} catch (ex: Exception) {
callBack.onError(ex)
when (result) {
is KtorfitResult.Success -> {
val response = result.response
ktorfit.httpClient.launch {
try {
val data = ktorfit.nextSuspendResponseConverter(
null,
typeData.typeArgs.first()
)?.convert(response)
callBack.onResponse(data!!, response)
} catch (ex: Exception) {
callBack.onError(ex)
}
}
}

is KtorfitResult.Failure -> {
callBack.onError(result.throwable)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.jensklingenberg.ktorfit.converter.builtin

import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.converter.Converter
import de.jensklingenberg.ktorfit.internal.TypeData
import io.ktor.client.call.*
Expand All @@ -25,17 +24,17 @@ public class FlowConverterFactory : Converter.Factory {
override fun convert(getResponse: suspend () -> HttpResponse): Flow<Any?> {
val requestType = typeData.typeArgs.first()
return flow {
val response = getResponse()
if (requestType.typeInfo.type == HttpResponse::class) {
emit(response)
} else {
val convertedBody = ktorfit.nextSuspendResponseConverter(
this@FlowConverterFactory,
typeData.typeArgs.first()
)?.convert(response)
?: response.body(typeData.typeArgs.first().typeInfo)
emit(convertedBody)
}
val response = getResponse()
if (requestType.typeInfo.type == HttpResponse::class) {
emit(response)
} else {
val convertedBody = ktorfit.nextSuspendResponseConverter(
this@FlowConverterFactory,
typeData.typeArgs.first()
)?.convert(response)
?: response.body(typeData.typeArgs.first().typeInfo)
emit(convertedBody)
}
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions ktorfit-lib-common/api/android/ktorfit-lib-common.api
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,28 @@ public final class de/jensklingenberg/ktorfit/converter/Converter$ResponseConver
}

public abstract interface class de/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter : de/jensklingenberg/ktorfit/converter/Converter {
public abstract fun convert (Lde/jensklingenberg/ktorfit/converter/KtorfitResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun convert (Lio/ktor/client/statement/HttpResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class de/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter$DefaultImpls {
public static fun convert (Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter;Lde/jensklingenberg/ktorfit/converter/KtorfitResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static fun getUpperBoundType (Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter;ILde/jensklingenberg/ktorfit/internal/TypeData;)Lde/jensklingenberg/ktorfit/internal/TypeData;
}

public abstract interface class de/jensklingenberg/ktorfit/converter/KtorfitResult {
}

public final class de/jensklingenberg/ktorfit/converter/KtorfitResult$Failure : de/jensklingenberg/ktorfit/converter/KtorfitResult {
public fun <init> (Ljava/lang/Throwable;)V
public final fun getThrowable ()Ljava/lang/Throwable;
}

public final class de/jensklingenberg/ktorfit/converter/KtorfitResult$Success : de/jensklingenberg/ktorfit/converter/KtorfitResult {
public fun <init> (Lio/ktor/client/statement/HttpResponse;)V
public final fun getResponse ()Lio/ktor/client/statement/HttpResponse;
}

public abstract interface class de/jensklingenberg/ktorfit/converter/SuspendResponseConverter : de/jensklingenberg/ktorfit/converter/request/CoreResponseConverter {
public abstract fun wrapSuspendResponse (Lde/jensklingenberg/ktorfit/internal/TypeData;Lkotlin/jvm/functions/Function1;Lde/jensklingenberg/ktorfit/Ktorfit;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
Expand Down
15 changes: 15 additions & 0 deletions ktorfit-lib-common/api/jvm/ktorfit-lib-common.api
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,28 @@ public final class de/jensklingenberg/ktorfit/converter/Converter$ResponseConver
}

public abstract interface class de/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter : de/jensklingenberg/ktorfit/converter/Converter {
public abstract fun convert (Lde/jensklingenberg/ktorfit/converter/KtorfitResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun convert (Lio/ktor/client/statement/HttpResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class de/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter$DefaultImpls {
public static fun convert (Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter;Lde/jensklingenberg/ktorfit/converter/KtorfitResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static fun getUpperBoundType (Lde/jensklingenberg/ktorfit/converter/Converter$SuspendResponseConverter;ILde/jensklingenberg/ktorfit/internal/TypeData;)Lde/jensklingenberg/ktorfit/internal/TypeData;
}

public abstract interface class de/jensklingenberg/ktorfit/converter/KtorfitResult {
}

public final class de/jensklingenberg/ktorfit/converter/KtorfitResult$Failure : de/jensklingenberg/ktorfit/converter/KtorfitResult {
public fun <init> (Ljava/lang/Throwable;)V
public final fun getThrowable ()Ljava/lang/Throwable;
}

public final class de/jensklingenberg/ktorfit/converter/KtorfitResult$Success : de/jensklingenberg/ktorfit/converter/KtorfitResult {
public fun <init> (Lio/ktor/client/statement/HttpResponse;)V
public final fun getResponse ()Lio/ktor/client/statement/HttpResponse;
}

public abstract interface class de/jensklingenberg/ktorfit/converter/SuspendResponseConverter : de/jensklingenberg/ktorfit/converter/request/CoreResponseConverter {
public abstract fun wrapSuspendResponse (Lde/jensklingenberg/ktorfit/internal/TypeData;Lkotlin/jvm/functions/Function1;Lde/jensklingenberg/ktorfit/Ktorfit;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ public class Ktorfit private constructor(
) {

/**
* Returns the next ResponseConverter after [currentFactory] that can handle [type]
* or null if no one found
* Returns the next response converter from the list of converter factories,
* starting from the specified current factory and matching the given type.
* @param currentFactory The current converter factory.
* @param type The type data to match.
* @return The next response converter, or null if not found.
*/
public fun nextResponseConverter(
currentFactory: Converter.Factory?,
Expand All @@ -44,8 +47,11 @@ public class Ktorfit private constructor(
}

/**
* Returns the next [SuspendResponseConverter] after [currentFactory] that can handle [type]
* or null if no one found
* Returns the next [SuspendResponseConverter] from the list of converter factories,
* starting from the specified current factory and matching the given type.
* @param currentFactory The current converter factory.
* @param type The type data to match.
* @return The next [SuspendResponseConverter], or null if not found.
*/
public fun nextSuspendResponseConverter(
currentFactory: Converter.Factory?,
Expand Down Expand Up @@ -122,6 +128,8 @@ public class Ktorfit private constructor(

/**
* Client that will be used for every request with object
* @param client The HTTP client to be used.
* @return The updated Builder instance.
*/
public fun httpClient(client: HttpClient): Builder = apply {
this._httpClient = client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ public class Response<T> private constructor(
public val isSuccessful: Boolean
get() = status.isSuccess()

/** The deserialized response body of a [successful][.isSuccessful] response. */
/** The deserialized response body of a [isSuccessful] response. */
public fun body(): T? {
return body
}

/** The raw response body of an [unsuccessful][.isSuccessful] response. */
/** The raw response body of an [unsuccessful] response. */
public fun errorBody(): Any? {
return errorBody
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,11 @@ public interface Converter<F, T> {
* e.g. fun getPost(): Call<Post>
* This is only needed for the return type of a non-suspend request, for every other case use [SuspendResponseConverter]
* @since 1.4.0
* @return the converted [HttpResponse]
*/
public interface ResponseConverter<F : HttpResponse, T> : Converter<HttpResponse, T> {

/**
*
* @param getResponse A suspend function that returns the HttpResponse to be converted.
* @return the converted [HttpResponse]
*/
public fun convert(getResponse: suspend () -> HttpResponse): T
Expand All @@ -27,15 +26,27 @@ public interface Converter<F, T> {
* Converter that transform the HTTPResponse within a suspend request
* e.g. suspend fun getPost(): Post
* @since 1.4.0
* @return the converted [HttpResponse]
*/
public interface SuspendResponseConverter<F : HttpResponse, T> : Converter<HttpResponse, T> {

/**
*
* @return the converted [HttpResponse]
*/
@Deprecated("Use convert(ktorfitResponse: KtorfitResponse)")
public suspend fun convert(response: HttpResponse): T

public suspend fun convert(result: KtorfitResult): T {
return when (result) {
is KtorfitResult.Failure -> {
throw result.throwable
}

is KtorfitResult.Success -> {
convert(result.response)
}
}
}
}

public interface RequestParameterConverter : Converter<Any, Any> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package de.jensklingenberg.ktorfit.converter

import io.ktor.client.statement.*

/**
* Represents the result from a Ktorfit request. */
public sealed interface KtorfitResult {
/**
* Represents a successful response.
* @property response The HTTP response.
*/
public class Success(public val response: HttpResponse) : KtorfitResult

/**
* Represents a failed response.
* @property throwable The throwable associated with the failure.
*/
public class Failure(public val throwable: Throwable) : KtorfitResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package de.jensklingenberg.ktorfit.converter.builtin

import de.jensklingenberg.ktorfit.Ktorfit
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.converter.Converter
import de.jensklingenberg.ktorfit.converter.KtorfitResult
import de.jensklingenberg.ktorfit.internal.TypeData
import io.ktor.client.call.*
import io.ktor.client.statement.*

internal class DefaultResponseClassSuspendConverter(private val typeData: TypeData, private val ktorfit: Ktorfit) :
Converter.SuspendResponseConverter<HttpResponse, Response<Any?>> {
override suspend fun convert(response: HttpResponse): Response<Any?> {
return convert(KtorfitResult.Success(response))
}

override suspend fun convert(result: KtorfitResult): Response<Any?> {
return when (result) {
is KtorfitResult.Success -> {
val typeInfo = typeData.typeArgs.first().typeInfo
val rawResponse = result.response
val code: Int = rawResponse.status.value
when {
code < 200 || code >= 300 -> {
val errorBody = rawResponse.body<Any>()
Response.error(errorBody, rawResponse)
}

code == 204 || code == 205 -> {
Response.success(null, rawResponse)
}

else -> {
val convertedBody = ktorfit.nextSuspendResponseConverter(
null,
typeData.typeArgs.first()
)?.convert(rawResponse)
?: rawResponse.body(typeInfo)
Response.success(convertedBody, rawResponse)
}
}
}

is KtorfitResult.Failure -> {
throw result.throwable
}
}
}
}
Loading

0 comments on commit 9b90863

Please sign in to comment.