Skip to content

Commit 4c0db20

Browse files
Gooolertomridder
andcommitted
Add support for Kotlin's Result
Co-authored-by: tomridder <[email protected]>
1 parent ab46542 commit 4c0db20

File tree

4 files changed

+154
-4
lines changed

4 files changed

+154
-4
lines changed

retrofit/kotlin-test/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
33
dependencies {
44
testImplementation projects.retrofit
55
testImplementation projects.retrofit.testHelpers
6+
testImplementation projects.retrofitConverters.gson
67
testImplementation libs.junit
78
testImplementation libs.assertj
89
testImplementation libs.mockwebserver

retrofit/kotlin-test/src/test/java/retrofit2/KotlinSuspendTest.kt

+59
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import org.junit.Assert.fail
3535
import org.junit.Ignore
3636
import org.junit.Rule
3737
import org.junit.Test
38+
import retrofit2.converter.gson.GsonConverterFactory
3839
import retrofit2.helpers.ToStringConverterFactory
3940
import retrofit2.http.GET
4041
import retrofit2.http.HEAD
@@ -59,6 +60,12 @@ class KotlinSuspendTest {
5960
@HEAD("/")
6061
suspend fun headUnit()
6162

63+
@GET("user")
64+
suspend fun getUser(): Result<User>
65+
66+
@HEAD("user")
67+
suspend fun headUser(): Result<Unit>
68+
6269
@GET("/{a}/{b}/{c}")
6370
suspend fun params(
6471
@Path("a") a: String,
@@ -70,6 +77,8 @@ class KotlinSuspendTest {
7077
suspend fun bodyWithCallType(): Call<String>
7178
}
7279

80+
data class User(val id: Int, val name: String, val email: String)
81+
7382
@Test fun body() {
7483
val retrofit = Retrofit.Builder()
7584
.baseUrl(server.url("/"))
@@ -389,6 +398,56 @@ class KotlinSuspendTest {
389398
}
390399
}
391400

401+
@Test fun returnResultType() = runBlocking {
402+
val responseBody = """
403+
{
404+
"id": 1,
405+
"name": "John Doe",
406+
"email": "[email protected]"
407+
}
408+
""".trimIndent()
409+
val retrofit = Retrofit.Builder()
410+
.baseUrl(server.url("/"))
411+
.addCallAdapterFactory(ResultCallAdapterFactory.create())
412+
.addConverterFactory(GsonConverterFactory.create())
413+
.build()
414+
val service = retrofit.create(Service::class.java)
415+
416+
// Successful response with body.
417+
server.enqueue(MockResponse().setBody(responseBody))
418+
service.getUser().let { result ->
419+
assertThat(result.isSuccess).isTrue()
420+
assertThat(result.getOrThrow().id).isEqualTo(1)
421+
assertThat(result.getOrThrow().name).isEqualTo("John Doe")
422+
assertThat(result.getOrThrow().email).isEqualTo("[email protected]")
423+
}
424+
425+
// Successful response without body.
426+
server.enqueue(MockResponse())
427+
service.headUser().let { result ->
428+
assertThat(result.isSuccess).isTrue()
429+
assertThat(result.getOrThrow()).isEqualTo(Unit)
430+
}
431+
432+
// Error response without body.
433+
server.enqueue(MockResponse().setResponseCode(404))
434+
service.getUser().let { result ->
435+
assertThat(result.isFailure).isTrue()
436+
assertThat(result.exceptionOrNull())
437+
.isInstanceOf(HttpException::class.java)
438+
.hasMessage("HTTP 404 Client Error")
439+
}
440+
441+
// Network error.
442+
server.shutdown()
443+
service.getUser().let { result ->
444+
assertThat(result.isFailure).isTrue()
445+
assertThat(result.exceptionOrNull()).isInstanceOf(IOException::class.java)
446+
}
447+
448+
Unit // Return type of runBlocking is Unit.
449+
}
450+
392451
@Suppress("EXPERIMENTAL_OVERRIDE")
393452
private object DirectUnconfinedDispatcher : CoroutineDispatcher() {
394453
override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

retrofit/src/main/java/retrofit2/HttpServiceMethod.java

+11-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.lang.reflect.ParameterizedType;
2424
import java.lang.reflect.Type;
2525
import javax.annotation.Nullable;
26+
import kotlin.Result;
2627
import kotlin.Unit;
2728
import kotlin.coroutines.Continuation;
2829
import okhttp3.ResponseBody;
@@ -42,7 +43,7 @@ static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotatio
4243
boolean continuationIsUnit = false;
4344

4445
Annotation[] annotations = method.getAnnotations();
45-
Type adapterType;
46+
final Type adapterType;
4647
if (isKotlinSuspendFunction) {
4748
Type[] parameterTypes = method.getGenericParameterTypes();
4849
Type responseType =
@@ -52,23 +53,29 @@ static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotatio
5253
// Unwrap the actual body type from Response<T>.
5354
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
5455
continuationWantsResponse = true;
56+
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
5557
} else {
56-
if (getRawType(responseType) == Call.class) {
58+
Class<?> rawType = getRawType(responseType);
59+
if (rawType == Call.class) {
5760
throw methodError(
5861
method,
5962
"Suspend functions should not return Call, as they already execute asynchronously.\n"
6063
+ "Change its return type to %s",
6164
Utils.getParameterUpperBound(0, (ParameterizedType) responseType));
6265
}
6366

67+
if (rawType == Result.class) {
68+
adapterType = responseType;
69+
} else {
70+
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
71+
}
72+
6473
continuationIsUnit = Utils.isUnit(responseType);
6574
// TODO figure out if type is nullable or not
6675
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
6776
// Find the entry for method
6877
// Determine if return type is nullable or not
6978
}
70-
71-
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
7279
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
7380
} else {
7481
adapterType = method.getGenericReturnType();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package retrofit2
2+
3+
import java.io.IOException
4+
import java.lang.reflect.ParameterizedType
5+
import java.lang.reflect.Type
6+
import okhttp3.Request
7+
import okio.Timeout
8+
9+
class ResultCallAdapterFactory private constructor() : CallAdapter.Factory() {
10+
override fun get(
11+
returnType: Type,
12+
annotations: Array<Annotation>,
13+
retrofit: Retrofit,
14+
): CallAdapter<*, *>? {
15+
if (getRawType(returnType) != Result::class.java) return null
16+
17+
check(returnType is ParameterizedType) {
18+
"Result must have a generic type (e.g., Result<T>)"
19+
}
20+
21+
return ResultCallAdapter<Any>(getParameterUpperBound(0, returnType))
22+
}
23+
24+
companion object {
25+
@JvmStatic
26+
fun create(): CallAdapter.Factory = ResultCallAdapterFactory()
27+
}
28+
}
29+
30+
class ResultCallAdapter<T>(
31+
private val responseType: Type,
32+
) : CallAdapter<T, Call<Result<T>>> {
33+
34+
override fun responseType(): Type = responseType
35+
36+
override fun adapt(call: Call<T>): Call<Result<T>> = ResultCall(call)
37+
}
38+
39+
class ResultCall<T>(private val delegate: Call<T>) : Call<Result<T>> {
40+
41+
override fun enqueue(callback: Callback<Result<T>>) {
42+
delegate.enqueue(object : Callback<T> {
43+
override fun onResponse(call: Call<T>, response: Response<T>) {
44+
val result = runCatching {
45+
if (response.isSuccessful) {
46+
response.body() ?: error("Response $response body is null.")
47+
} else {
48+
throw HttpException(response)
49+
}
50+
}
51+
callback.onResponse(this@ResultCall, Response.success(result))
52+
}
53+
54+
override fun onFailure(call: Call<T>, t: Throwable) {
55+
callback.onResponse(this@ResultCall, Response.success(Result.failure(t)))
56+
}
57+
})
58+
}
59+
60+
override fun execute(): Response<Result<T>> {
61+
val result = runCatching {
62+
val response = delegate.execute()
63+
if (response.isSuccessful) {
64+
response.body() ?: error("Response $response body is null.")
65+
} else {
66+
throw IOException("Unexpected error: ${response.errorBody()?.string()}")
67+
}
68+
}
69+
return Response.success(result)
70+
}
71+
72+
override fun isExecuted(): Boolean = delegate.isExecuted
73+
74+
override fun clone(): ResultCall<T> = ResultCall(delegate.clone())
75+
76+
override fun isCanceled(): Boolean = delegate.isCanceled
77+
78+
override fun cancel(): Unit = delegate.cancel()
79+
80+
override fun request(): Request = delegate.request()
81+
82+
override fun timeout(): Timeout = delegate.timeout()
83+
}

0 commit comments

Comments
 (0)