import arrow.core.Either import okhttp3.Request import okio.Timeout import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit import java.io.IOException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type /** * Custom [CallAdapter.Factory] to handle Retrofit [Response] through [Either] type * * Original idea taken from: * https://proandroiddev.com/retrofit-calladapter-for-either-type-2145781e1c20 */ internal class EitherCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array, retrofit: Retrofit ): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) return null check(returnType is ParameterizedType) { "Return type must be a parameterized type." } val responseType = getParameterUpperBound(0, returnType) if (getRawType(responseType) != Either::class.java) return null check(responseType is ParameterizedType) { "Response type must be a parameterized type." } val leftType = getParameterUpperBound(0, responseType) if (getRawType(leftType) != ApiError::class.java) return null val rightType = getParameterUpperBound(1, responseType) return EitherCallAdapter(rightType) } } private class EitherCallAdapter( private val successType: Type ) : CallAdapter>> { override fun adapt(call: Call): Call> = EitherCall(call, successType) override fun responseType(): Type = successType } class EitherCall( private val delegate: Call, private val successType: Type ) : Call> { override fun enqueue(callback: Callback>) = delegate.enqueue( object : Callback { override fun onResponse(call: Call, response: Response) { callback.onResponse(this@EitherCall, Response.success(response.toEither())) } override fun onFailure(call: Call, throwable: Throwable) { val error = when (throwable) { is IOException -> NetworkError(throwable) else -> UnknownApiError(throwable) } callback.onResponse(this@EitherCall, Response.success(Either.Left(error))) } } ) override fun isExecuted(): Boolean = delegate.isExecuted override fun clone(): Call> = EitherCall(delegate.clone(), successType) override fun isCanceled(): Boolean = delegate.isCanceled override fun cancel() = delegate.cancel() override fun execute(): Response> = throw UnsupportedOperationException() override fun request(): Request = delegate.request() override fun timeout(): Timeout = delegate.timeout() private fun Response.toEither(): Either { // Http error response (4xx - 5xx) if (!isSuccessful) { val errorBody = errorBody()?.string() ?: "" return Either.Left(HttpError(code(), errorBody)) } // Http success response with body body()?.let { body -> return Either.Right(body) } // if we defined Unit as success type it means we expected no response body // e.g. in case of 204 No Content return if (successType == Unit::class.java) { @Suppress("UNCHECKED_CAST") Either.Right(Unit) as Either } else { @Suppress("UNCHECKED_CAST") Either.Left(UnknownError("Response body was null")) as Either } } } sealed class ApiError data class HttpError(val code: Int, val body: String) : ApiError() data class NetworkError(val throwable: Throwable) : ApiError() data class UnknownApiError(val throwable: Throwable) : ApiError()