Skip to content

Commit 49982e0

Browse files
authored
Merge pull request #3 from haroldadmin/suspend-fun-support
feat: Add support for suspending functions with NetworkResponse return type
2 parents dd63d51 + d27b9ba commit 49982e0

17 files changed

+562
-44
lines changed

build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
plugins {
2-
id("org.jetbrains.kotlin.jvm").version("1.3.41")
2+
id("org.jetbrains.kotlin.jvm").version("1.3.50")
33
maven
44
}
55

@@ -15,9 +15,9 @@ val test by tasks.getting(Test::class) {
1515

1616
dependencies {
1717
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
18-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0-RC")
19-
implementation("com.squareup.retrofit2:retrofit:2.6.0")
20-
implementation("com.squareup.okhttp3:okhttp:4.0.1")
18+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0")
19+
implementation("com.squareup.retrofit2:retrofit:2.6.1")
20+
implementation("com.squareup.okhttp3:okhttp:4.2.0")
2121

2222
testImplementation("com.squareup.okhttp3:mockwebserver:4.0.1")
2323
testImplementation("com.google.guava:guava:26.0-jre")

src/main/kotlin/com/haroldadmin/cnradapter/CoroutinesNetworkResponseAdapter.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ import java.lang.reflect.Type
1717
* @constructor Creates a CoroutinesNetworkResponseAdapter
1818
*/
1919

20-
private const val UNKNOWN_ERROR_RESPONSE_CODE = 520
21-
20+
@Deprecated(
21+
message = "This class should not be used anymore. Pick DeferredNetworkResponseAdapter or NetworkResponseAdapter based on your needs",
22+
replaceWith = ReplaceWith(
23+
expression = "DeferredNetworkResponseAdapter",
24+
imports = ["com.haroldadmin.cnradapter.DeferredNetworkResponseAdapter"]
25+
),
26+
level = DeprecationLevel.WARNING
27+
)
2228
internal class CoroutinesNetworkResponseAdapter<T : Any, U : Any>(
2329
private val successBodyType: Type,
2430
private val errorConverter: Converter<ResponseBody, U>
@@ -48,6 +54,7 @@ internal class CoroutinesNetworkResponseAdapter<T : Any, U : Any>(
4854

4955
call.enqueue(object : Callback<T> {
5056
override fun onFailure(call: Call<T>, throwable: Throwable) {
57+
// TODO Use ErrorExtraction methods here
5158
when (throwable) {
5259

5360
is IOException -> deferred.complete(NetworkResponse.NetworkError(throwable))

src/main/kotlin/com/haroldadmin/cnradapter/CoroutinesNetworkResponseAdapterFactory.kt

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@ import java.lang.reflect.Type
1010
/**
1111
* A Factory class to create instances of [CoroutinesNetworkResponseAdapter]
1212
*/
13+
@Deprecated(
14+
message = "This class should not be used anymore. Replace with NetworkResponseAdapterFactory",
15+
replaceWith = ReplaceWith(
16+
expression = "NetworkResponseAdapterFactory",
17+
imports = ["com.haroldadmin.cnradapter.NetworkResponseAdapterFactory"]
18+
),
19+
level = DeprecationLevel.WARNING
20+
)
1321
class CoroutinesNetworkResponseAdapterFactory private constructor() : CallAdapter.Factory() {
1422

1523
companion object {
1624
@JvmStatic
1725
@JvmName("create")
18-
operator fun invoke() = CoroutinesNetworkResponseAdapterFactory()
26+
@Suppress("DEPRECATION")
27+
operator fun invoke(): CoroutinesNetworkResponseAdapterFactory {
28+
throw UnsupportedOperationException("Use NetworkResponseAdapterFactory instead of this class")
29+
}
1930
}
2031

2132
/**
@@ -26,22 +37,15 @@ class CoroutinesNetworkResponseAdapterFactory private constructor() : CallAdapte
2637
if (Deferred::class.java != rawType) {
2738
return null
2839
}
29-
if (returnType !is ParameterizedType) {
30-
throw IllegalStateException(
31-
"Deferred return must be parameterized as Deferred<Foo> or Deferred<out Foo>"
32-
)
33-
}
40+
41+
check(returnType is ParameterizedType) { "Deferred return must be parameterized as Deferred<Foo> or Deferred<out Foo>" }
3442

3543
val containerType = getParameterUpperBound(0, returnType)
3644
if (getRawType(containerType) != NetworkResponse::class.java) {
3745
return null
3846
}
3947

40-
if (containerType !is ParameterizedType) {
41-
throw IllegalStateException(
42-
"NetworkResponse must be parameterized as NetworkResponse<SuccessBody, ErrorBody>"
43-
)
44-
}
48+
check(containerType is ParameterizedType) { "NetworkResponse must be parameterized as NetworkResponse<SuccessBody, ErrorBody>" }
4549

4650
val successBodyType = getParameterUpperBound(0, containerType)
4751
val errorBodyType = getParameterUpperBound(1, containerType)
@@ -50,6 +54,7 @@ class CoroutinesNetworkResponseAdapterFactory private constructor() : CallAdapte
5054
errorBodyType,
5155
annotations
5256
)
57+
@Suppress("DEPRECATION")
5358
return CoroutinesNetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
5459
}
5560
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.haroldadmin.cnradapter
2+
import kotlinx.coroutines.CompletableDeferred
3+
import kotlinx.coroutines.Deferred
4+
import okhttp3.ResponseBody
5+
import retrofit2.*
6+
import java.io.IOException
7+
import java.lang.reflect.Type
8+
9+
/**
10+
* A Retrofit converter to return objects wrapped in [NetworkResponse] class
11+
*
12+
* @param T The type of the successful response model
13+
* @param U The type of the error response model
14+
* @param successBodyType The type of the successful response model in [NetworkResponse]
15+
* @param errorConverter The converter to extract error information from [ResponseBody]
16+
* @constructor Creates a DeferredNetworkResponseAdapter
17+
*/
18+
19+
internal class DeferredNetworkResponseAdapter<T : Any, U : Any>(
20+
private val successBodyType: Type,
21+
private val errorConverter: Converter<ResponseBody, U>
22+
) : CallAdapter<T, Deferred<NetworkResponse<T, U>>> {
23+
24+
/**
25+
* This is used to determine the parameterize type of the object
26+
* being handled by this adapter. For example, the response type
27+
* in Call<Repo> is Repo.
28+
*/
29+
override fun responseType(): Type = successBodyType
30+
31+
/**
32+
* Returns an instance of [T] by modifying a [Call] object
33+
*
34+
* @param call The call object to be converted
35+
* @return The T instance wrapped in a [NetworkResponse] class wrapped in [Deferred]
36+
*/
37+
override fun adapt(call: Call<T>): Deferred<NetworkResponse<T, U>> {
38+
val deferred = CompletableDeferred<NetworkResponse<T, U>>()
39+
40+
deferred.invokeOnCompletion {
41+
if (deferred.isCancelled) {
42+
call.cancel()
43+
}
44+
}
45+
46+
call.enqueue(object : Callback<T> {
47+
override fun onFailure(call: Call<T>, throwable: Throwable) {
48+
try {
49+
val networkResponse = throwable.extractNetworkResponse<T, U>(errorConverter)
50+
deferred.complete(networkResponse)
51+
} catch (t: Throwable) {
52+
deferred.completeExceptionally(t)
53+
}
54+
}
55+
56+
override fun onResponse(call: Call<T>, response: Response<T>) {
57+
val headers = response.headers()
58+
val responseCode = response.code()
59+
val body = response.body()
60+
body?.let {
61+
deferred.complete(NetworkResponse.Success(it, headers))
62+
} ?: deferred.complete(NetworkResponse.ServerError(null, responseCode, headers))
63+
}
64+
})
65+
66+
return deferred
67+
}
68+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.haroldadmin.cnradapter
2+
3+
import okhttp3.ResponseBody
4+
import retrofit2.Converter
5+
import retrofit2.HttpException
6+
import java.io.IOException
7+
8+
internal const val UNKNOWN_ERROR_RESPONSE_CODE = 520
9+
10+
internal fun <S : Any, E : Any> HttpException.extractFromHttpException(errorConverter: Converter<ResponseBody, E>): NetworkResponse<S, E> {
11+
val error = response()?.errorBody()
12+
val responseCode = response()?.code() ?: UNKNOWN_ERROR_RESPONSE_CODE
13+
val headers = response()?.headers()
14+
val errorBody = when {
15+
error == null -> null // No error content available
16+
error.contentLength() == 0L -> null // Error content is empty
17+
else -> try {
18+
// There is error content present, so we should try to extract it
19+
errorConverter.convert(error)
20+
} catch (e: Exception) {
21+
// If unable to extract content, return with a null body and don't parse further
22+
return NetworkResponse.ServerError(null, responseCode, headers)
23+
}
24+
}
25+
return NetworkResponse.ServerError(errorBody, responseCode, headers)
26+
}
27+
28+
internal fun <S : Any, E : Any> Throwable.extractNetworkResponse(errorConverter: Converter<ResponseBody, E>): NetworkResponse<S, E> {
29+
return when (this) {
30+
is IOException -> NetworkResponse.NetworkError(this)
31+
is HttpException -> extractFromHttpException<S, E>(errorConverter)
32+
else -> throw this
33+
}
34+
}

src/main/kotlin/com/haroldadmin/cnradapter/Extensions.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import kotlinx.coroutines.delay
1515
* @return The NetworkResponse value whether it be successful or failed after retrying
1616
*/
1717
suspend inline fun <T : Any, U : Any> executeWithRetry(
18-
times: Int = 10,
19-
initialDelay: Long = 100, // 0.1 second
20-
maxDelay: Long = 1000, // 1 second
21-
factor: Double = 2.0,
22-
block: () -> NetworkResponse<T, U>
18+
times: Int = 10,
19+
initialDelay: Long = 100, // 0.1 second
20+
maxDelay: Long = 1000, // 1 second
21+
factor: Double = 2.0,
22+
block: suspend () -> NetworkResponse<T, U>
2323
): NetworkResponse<T, U> {
2424
var currentDelay = initialDelay
2525
repeat(times - 1) {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.haroldadmin.cnradapter
2+
3+
import okhttp3.ResponseBody
4+
import retrofit2.Call
5+
import retrofit2.CallAdapter
6+
import retrofit2.Converter
7+
import java.lang.reflect.Type
8+
9+
class NetworkResponseAdapter<S : Any, E : Any>(
10+
private val successType: Type,
11+
private val errorBodyConverter: Converter<ResponseBody, E>
12+
) : CallAdapter<S, Call<NetworkResponse<S, E>>> {
13+
14+
override fun responseType(): Type = successType
15+
16+
override fun adapt(call: Call<S>): Call<NetworkResponse<S, E>> {
17+
return NetworkResponseCall(call, errorBodyConverter)
18+
}
19+
}
20+
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.haroldadmin.cnradapter
2+
3+
import kotlinx.coroutines.Deferred
4+
import retrofit2.Call
5+
import retrofit2.CallAdapter
6+
import retrofit2.Retrofit
7+
import java.lang.reflect.ParameterizedType
8+
import java.lang.reflect.Type
9+
10+
class NetworkResponseAdapterFactory : CallAdapter.Factory() {
11+
12+
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
13+
14+
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
15+
16+
val containerType = getParameterUpperBound(0, returnType)
17+
if (getRawType(containerType) != NetworkResponse::class.java) {
18+
return null
19+
}
20+
21+
check(containerType is ParameterizedType) { "$containerType must be parameterized. Raw types are not supported" }
22+
23+
val (successBodyType, errorBodyType) = containerType.getBodyTypes()
24+
val errorBodyConverter = retrofit.nextResponseBodyConverter<Any>(null, errorBodyType, annotations)
25+
26+
return when (getRawType(returnType)) {
27+
Deferred::class.java -> {
28+
DeferredNetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
29+
}
30+
31+
Call::class.java -> {
32+
NetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
33+
}
34+
else -> null
35+
}
36+
}
37+
38+
private fun ParameterizedType.getBodyTypes(): Pair<Type, Type> {
39+
val successType = getParameterUpperBound(0, this)
40+
val errorType = getParameterUpperBound(1, this)
41+
return successType to errorType
42+
}
43+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.haroldadmin.cnradapter
2+
3+
import okhttp3.Request
4+
import okhttp3.ResponseBody
5+
import retrofit2.Call
6+
import retrofit2.Callback
7+
import retrofit2.Converter
8+
import retrofit2.Response
9+
10+
internal class NetworkResponseCall<S : Any, E : Any>(
11+
private val backingCall: Call<S>,
12+
private val errorConverter: Converter<ResponseBody, E>
13+
) : Call<NetworkResponse<S, E>> {
14+
15+
override fun enqueue(callback: Callback<NetworkResponse<S, E>>) = synchronized(this) {
16+
backingCall.enqueue(object : Callback<S> {
17+
override fun onResponse(call: Call<S>, response: Response<S>) {
18+
val body = response.body()
19+
if (body != null) {
20+
callback.onResponse(this@NetworkResponseCall, Response.success(NetworkResponse.Success(body, response.headers())))
21+
} else {
22+
callback.onResponse(this@NetworkResponseCall, Response.success(NetworkResponse.ServerError(null, response.code(), response.headers())))
23+
}
24+
}
25+
26+
override fun onFailure(call: Call<S>, throwable: Throwable) {
27+
val networkResponse = throwable.extractNetworkResponse<S, E>(errorConverter)
28+
callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
29+
}
30+
})
31+
}
32+
33+
override fun isExecuted(): Boolean = synchronized(this) {
34+
backingCall.isExecuted
35+
}
36+
37+
override fun clone(): Call<NetworkResponse<S, E>> = NetworkResponseCall(backingCall.clone(), errorConverter)
38+
39+
override fun isCanceled(): Boolean = synchronized(this) {
40+
backingCall.isCanceled
41+
}
42+
43+
override fun cancel() = synchronized(this) {
44+
backingCall.cancel()
45+
}
46+
47+
override fun execute(): Response<NetworkResponse<S, E>> {
48+
throw UnsupportedOperationException("Network Response call does not support synchronous execution")
49+
}
50+
51+
override fun request(): Request = backingCall.request()
52+
}

src/test/kotlin/com/haroldadmin/cnradapter/CancelTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import java.io.IOException
1111
internal class CancelTest : DescribeSpec({
1212

1313
val mockWebServer = MockWebServer()
14-
val factory = CoroutinesNetworkResponseAdapterFactory()
14+
val factory = NetworkResponseAdapterFactory()
1515
val retrofit = Retrofit.Builder()
1616
.baseUrl(mockWebServer.url("/"))
1717
.addConverterFactory(StringConverterFactory())

0 commit comments

Comments
 (0)