From ef13d4ad12ebf9788b9f94ea89200b5997eefaa2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 21 Dec 2023 22:12:39 +0800 Subject: [PATCH] Add support for Kotlin's Result Co-authored-by: tomridder <1402709211@qq.com> --- retrofit/kotlin-test/build.gradle | 1 + .../test/java/retrofit2/KotlinSuspendTest.kt | 55 ++++++++++++ .../java/retrofit2/HttpServiceMethod.java | 15 +++- .../retrofit2/ResultCallAdapterFactory.kt | 83 +++++++++++++++++++ 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 retrofit/src/main/java/retrofit2/ResultCallAdapterFactory.kt diff --git a/retrofit/kotlin-test/build.gradle b/retrofit/kotlin-test/build.gradle index b39ed4e17c..2d9716b1ae 100644 --- a/retrofit/kotlin-test/build.gradle +++ b/retrofit/kotlin-test/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'org.jetbrains.kotlin.jvm' dependencies { testImplementation projects.retrofit testImplementation projects.retrofit.testHelpers + testImplementation projects.retrofitConverters.gson testImplementation libs.junit testImplementation libs.assertj testImplementation libs.mockwebserver diff --git a/retrofit/kotlin-test/src/test/java/retrofit2/KotlinSuspendTest.kt b/retrofit/kotlin-test/src/test/java/retrofit2/KotlinSuspendTest.kt index b52f5e14f4..8a49283409 100644 --- a/retrofit/kotlin-test/src/test/java/retrofit2/KotlinSuspendTest.kt +++ b/retrofit/kotlin-test/src/test/java/retrofit2/KotlinSuspendTest.kt @@ -31,6 +31,7 @@ import org.junit.Assert.fail import org.junit.Ignore import org.junit.Rule import org.junit.Test +import retrofit2.converter.gson.GsonConverterFactory import retrofit2.helpers.ToStringConverterFactory import retrofit2.http.GET import retrofit2.http.HEAD @@ -49,6 +50,8 @@ class KotlinSuspendTest { @GET("/") suspend fun response(): Response @GET("/") suspend fun unit() @HEAD("/") suspend fun headUnit() + @GET("user") suspend fun getUser(): Result + @HEAD("user") suspend fun headUser(): Result @GET("/{a}/{b}/{c}") suspend fun params( @@ -60,6 +63,8 @@ class KotlinSuspendTest { @GET("/") suspend fun bodyWithCallType(): Call } + data class User(val id: Int, val name: String, val email: String) + @Test fun body() { val retrofit = Retrofit.Builder() .baseUrl(server.url("/")) @@ -374,6 +379,56 @@ class KotlinSuspendTest { } } + @Test fun returnResultType() = runBlocking { + val responseBody = """ + { + "id": 1, + "name": "John Doe", + "email": "john.doe@example.com" + } + """.trimIndent() + val retrofit = Retrofit.Builder() + .baseUrl(server.url("/")) + .addCallAdapterFactory(ResultCallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .build() + val service = retrofit.create(Service::class.java) + + // Successful response with body. + server.enqueue(MockResponse().setBody(responseBody)) + service.getUser().let { result -> + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrThrow().id).isEqualTo(1) + assertThat(result.getOrThrow().name).isEqualTo("John Doe") + assertThat(result.getOrThrow().email).isEqualTo("john.doe@example.com") + } + + // Successful response without body. + server.enqueue(MockResponse()) + service.headUser().let { result -> + assertThat(result.isSuccess).isTrue() + assertThat(result.getOrThrow()).isEqualTo(Unit) + } + + // Error response without body. + server.enqueue(MockResponse().setResponseCode(404)) + service.getUser().let { result -> + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()) + .isInstanceOf(HttpException::class.java) + .hasMessage("HTTP 404 Client Error") + } + + // Network error. + server.shutdown() + service.getUser().let { result -> + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IOException::class.java) + } + + Unit // Return type of runBlocking is Unit. + } + @Suppress("EXPERIMENTAL_OVERRIDE") private object DirectUnconfinedDispatcher : CoroutineDispatcher() { override fun isDispatchNeeded(context: CoroutineContext): Boolean = false diff --git a/retrofit/src/main/java/retrofit2/HttpServiceMethod.java b/retrofit/src/main/java/retrofit2/HttpServiceMethod.java index 0f7bbc0904..5e156e8c2d 100644 --- a/retrofit/src/main/java/retrofit2/HttpServiceMethod.java +++ b/retrofit/src/main/java/retrofit2/HttpServiceMethod.java @@ -23,6 +23,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import javax.annotation.Nullable; +import kotlin.Result; import kotlin.Unit; import kotlin.coroutines.Continuation; import okhttp3.ResponseBody; @@ -42,7 +43,7 @@ static HttpServiceMethod parseAnnotatio boolean continuationIsUnit = false; Annotation[] annotations = method.getAnnotations(); - Type adapterType; + final Type adapterType; if (isKotlinSuspendFunction) { Type[] parameterTypes = method.getGenericParameterTypes(); Type responseType = @@ -52,22 +53,28 @@ static HttpServiceMethod parseAnnotatio // Unwrap the actual body type from Response. responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType); continuationWantsResponse = true; + adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType); } else { - if (getRawType(responseType) == Call.class) { + Class rawType = getRawType(responseType); + if (rawType == Call.class) { throw methodError( method, "Suspend functions should not return Call, as they already execute asynchronously.\n" + "Change its return type to %s", Utils.getParameterUpperBound(0, (ParameterizedType) responseType)); } + if (rawType == Result.class) { + adapterType = responseType; + } else { + adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType); + } + continuationIsUnit = Utils.isUnit(responseType); // TODO figure out if type is nullable or not // Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class) // Find the entry for method // Determine if return type is nullable or not } - - adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType); annotations = SkipCallbackExecutorImpl.ensurePresent(annotations); } else { adapterType = method.getGenericReturnType(); diff --git a/retrofit/src/main/java/retrofit2/ResultCallAdapterFactory.kt b/retrofit/src/main/java/retrofit2/ResultCallAdapterFactory.kt new file mode 100644 index 0000000000..eea1bb834d --- /dev/null +++ b/retrofit/src/main/java/retrofit2/ResultCallAdapterFactory.kt @@ -0,0 +1,83 @@ +package retrofit2 + +import java.io.IOException +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import okhttp3.Request +import okio.Timeout + +class ResultCallAdapterFactory private constructor() : CallAdapter.Factory() { + override fun get( + returnType: Type, + annotations: Array, + retrofit: Retrofit + ): CallAdapter<*, *>? { + if (getRawType(returnType) != Result::class.java) return null + + check(returnType is ParameterizedType) { + "Result must have a generic type (e.g., Result)" + } + + return ResultCallAdapter(getParameterUpperBound(0, returnType)) + } + + companion object { + @JvmStatic + fun create(): CallAdapter.Factory = ResultCallAdapterFactory() + } +} + +class ResultCallAdapter( + private val responseType: Type +) : CallAdapter>> { + + override fun responseType(): Type = responseType + + override fun adapt(call: Call): Call> = ResultCall(call) +} + +class ResultCall(private val delegate: Call) : Call> { + + override fun enqueue(callback: Callback>) { + delegate.enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val result = runCatching { + if (response.isSuccessful) { + response.body() ?: error("Response $response body is null.") + } else { + throw HttpException(response) + } + } + callback.onResponse(this@ResultCall, Response.success(result)) + } + + override fun onFailure(call: Call, t: Throwable) { + callback.onResponse(this@ResultCall, Response.success(Result.failure(t))) + } + }) + } + + override fun execute(): Response> { + val result = runCatching { + val response = delegate.execute() + if (response.isSuccessful) { + response.body() ?: error("Response $response body is null.") + } else { + throw IOException("Unexpected error: ${response.errorBody()?.string()}") + } + } + return Response.success(result) + } + + override fun isExecuted(): Boolean = delegate.isExecuted + + override fun clone(): ResultCall = ResultCall(delegate.clone()) + + override fun isCanceled(): Boolean = delegate.isCanceled + + override fun cancel(): Unit = delegate.cancel() + + override fun request(): Request = delegate.request() + + override fun timeout(): Timeout = delegate.timeout() +}