Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retrofit adapter: allow null response body for methods returning Unit #2625

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -23,13 +23,14 @@ internal class ArrowEitherCallAdapter<E, R>(
private val errorConverter: Converter<ResponseBody, E> =
retrofit.responseBodyConverter(errorType, arrayOfNulls(0))

override fun adapt(call: Call<R>): Call<Either<E, R>> = EitherCall(call, errorConverter)
override fun adapt(call: Call<R>): Call<Either<E, R>> = EitherCall(call, errorConverter, bodyType)

override fun responseType(): Type = bodyType

class EitherCall<E, R>(
private val original: Call<R>,
private val errorConverter: Converter<ResponseBody, E>
private val errorConverter: Converter<ResponseBody, E>,
private val bodyType: Type
) : Call<Either<E, R>> {

override fun enqueue(callback: Callback<Either<E, R>>) {
Expand All @@ -44,6 +45,7 @@ internal class ArrowEitherCallAdapter<E, R>(
callback,
this@EitherCall,
errorConverter,
bodyType,
response,
{ body, _ ->
Response.success(response.code(), body.right())
Expand All @@ -60,7 +62,7 @@ internal class ArrowEitherCallAdapter<E, R>(

override fun timeout(): Timeout = original.timeout()

override fun clone(): Call<Either<E, R>> = EitherCall(original.clone(), errorConverter)
override fun clone(): Call<Either<E, R>> = EitherCall(original.clone(), errorConverter, bodyType)

override fun isCanceled(): Boolean = original.isCanceled

Expand Down
@@ -1,6 +1,5 @@
package arrow.retrofit.adapter.either

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import okhttp3.Request
Expand All @@ -23,13 +22,14 @@ internal class ArrowResponseECallAdapter<E, R>(
private val errorConverter: Converter<ResponseBody, E> =
retrofit.responseBodyConverter(errorType, arrayOfNulls(0))

override fun adapt(call: Call<R>): Call<ResponseE<E, R>> = ResponseECall(call, errorConverter)
override fun adapt(call: Call<R>): Call<ResponseE<E, R>> = ResponseECall(call, errorConverter, bodyType)

override fun responseType(): Type = bodyType

class ResponseECall<E, R>(
private val original: Call<R>,
private val errorConverter: Converter<ResponseBody, E>
private val errorConverter: Converter<ResponseBody, E>,
private val bodyType: Type
) : Call<ResponseE<E, R>> {

override fun enqueue(callback: Callback<ResponseE<E, R>>) {
Expand All @@ -44,13 +44,13 @@ internal class ArrowResponseECallAdapter<E, R>(
callback,
this@ResponseECall,
errorConverter,
bodyType,
response,
{ body, responseT ->
val bodyE: Either<E, R> = body.right()
Response.success(responseT.code(), ResponseE(responseT.raw(), bodyE))
Response.success(responseT.code(), ResponseE(responseT.raw(), body.right()))
},
{ errorBody, responseV ->
Response.success<ResponseE<E, R>>(ResponseE(responseV.raw(), errorBody.left()))
Response.success(ResponseE(responseV.raw(), errorBody.left()))
}
)
}
Expand All @@ -61,7 +61,7 @@ internal class ArrowResponseECallAdapter<E, R>(

override fun timeout(): Timeout = original.timeout()

override fun clone(): Call<ResponseE<E, R>> = ResponseECall(original.clone(), errorConverter)
override fun clone(): Call<ResponseE<E, R>> = ResponseECall(original.clone(), errorConverter, bodyType)

override fun isCanceled(): Boolean = original.isCanceled

Expand Down
Expand Up @@ -56,7 +56,10 @@ public class EitherCallAdapterFactory : CallAdapter.Factory() {
}
}

private fun eitherAdapter(returnType: ParameterizedType, retrofit: Retrofit): CallAdapter<Type, out Call<out Any>>? {
private fun eitherAdapter(
returnType: ParameterizedType,
retrofit: Retrofit
): CallAdapter<Type, out Call<out Any>>? {
val wrapperType = getParameterUpperBound(0, returnType)
return when (getRawType(wrapperType)) {
Either::class.java -> {
Expand Down
Expand Up @@ -5,19 +5,28 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Converter
import retrofit2.Response
import java.lang.reflect.Type

internal inline fun <E, R, T> onResponseFn(
callback: Callback<T>,
call: Call<T>,
errorConverter: Converter<ResponseBody, E>,
bodyType: Type,
response: Response<R>,
newResponseFn: (R, Response<R>) -> Response<T>,
errorResponseFn: (E, Response<R>) -> Response<T>
) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
callback.onFailure(call, IllegalStateException("Null body found!"))
// if we defined Unit as body type it means we expected no response body
// e.g. in case of 204 No Content
if (bodyType == Unit::class.java) {
@Suppress("UNCHECKED_CAST")
callback.onResponse(call, newResponseFn(Unit as R, response))
} else {
callback.onFailure(call, IllegalStateException("Null body found!"))
}
} else {
callback.onResponse(call, newResponseFn(body, response))
}
Expand Down
Expand Up @@ -41,6 +41,22 @@ class ArrowEitherCallAdapterTest : UnitSpec() {
body shouldBe ResponseMock("Arrow rocks").right()
}

"should return Unit when service method returns Unit and null body received" {
server.enqueue(MockResponse().setResponseCode(204))

val body = service.postSomething("Sample string")

body shouldBe Unit.right()
}

"should return Unit when service method returns Unit and JSON body received" {
server.enqueue(MockResponse().setBody("""{"response":"Arrow rocks"}"""))

val body = service.postSomething("Sample string")

body shouldBe Unit.right()
}

"should return ErrorMock for 400 with valid JSON" {
server.enqueue(MockResponse().setBody("""{"errorCode":666}""").setResponseCode(400))

Expand Down
Expand Up @@ -44,6 +44,28 @@ class ArrowResponseEAdapterTest : UnitSpec() {
}
}

"should return Unit when service method returns Unit and null body received" {
server.enqueue(MockResponse().setResponseCode(204))

val responseE = service.postSomethingResponseE("Sample string")

with(responseE) {
code shouldBe 204
body shouldBe Unit.right()
}
}

"should return Unit when service method returns Unit and JSON body received" {
server.enqueue(MockResponse().setBody("""{"response":"Arrow rocks"}"""))

val responseE = service.postSomethingResponseE("Sample string")

with(responseE) {
code shouldBe 200
body shouldBe Unit.right()
}
}

"should return ErrorMock for 400 with valid JSON" {
server.enqueue(MockResponse().setBody("""{"errorCode":42}""").setResponseCode(400))

Expand Down
Expand Up @@ -4,13 +4,21 @@ import arrow.core.Either
import arrow.retrofit.adapter.either.ResponseE
import arrow.retrofit.adapter.mock.ErrorMock
import arrow.retrofit.adapter.mock.ResponseMock
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST

interface SuspendApiTestClient {

@GET("/")
suspend fun getEither(): Either<ErrorMock, ResponseMock>

@POST("/")
suspend fun postSomething(@Body something: String): Either<ErrorMock, Unit>

@GET("/")
suspend fun getResponseE(): ResponseE<ErrorMock, ResponseMock>

@POST("/")
suspend fun postSomethingResponseE(@Body something: String): ResponseE<ErrorMock, Unit>
}