Skip to content

Commit

Permalink
Retrofit adapter: allow null response body for methods returning Unit (
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszkalnik authored Feb 17, 2022
1 parent 2fbcfa7 commit cbbf3e4
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 12 deletions.
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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>
}

0 comments on commit cbbf3e4

Please sign in to comment.