diff --git a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapter.kt b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapter.kt index 0127db812aa..d60464a6946 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapter.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapter.kt @@ -23,13 +23,14 @@ internal class ArrowEitherCallAdapter( private val errorConverter: Converter = retrofit.responseBodyConverter(errorType, arrayOfNulls(0)) - override fun adapt(call: Call): Call> = EitherCall(call, errorConverter) + override fun adapt(call: Call): Call> = EitherCall(call, errorConverter, bodyType) override fun responseType(): Type = bodyType class EitherCall( private val original: Call, - private val errorConverter: Converter + private val errorConverter: Converter, + private val bodyType: Type ) : Call> { override fun enqueue(callback: Callback>) { @@ -44,6 +45,7 @@ internal class ArrowEitherCallAdapter( callback, this@EitherCall, errorConverter, + bodyType, response, { body, _ -> Response.success(response.code(), body.right()) @@ -60,7 +62,7 @@ internal class ArrowEitherCallAdapter( override fun timeout(): Timeout = original.timeout() - override fun clone(): Call> = EitherCall(original.clone(), errorConverter) + override fun clone(): Call> = EitherCall(original.clone(), errorConverter, bodyType) override fun isCanceled(): Boolean = original.isCanceled diff --git a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowResponseECallAdapter.kt b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowResponseECallAdapter.kt index 45181d9b436..c1157393401 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowResponseECallAdapter.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/ArrowResponseECallAdapter.kt @@ -1,6 +1,5 @@ package arrow.retrofit.adapter.either -import arrow.core.Either import arrow.core.left import arrow.core.right import okhttp3.Request @@ -23,13 +22,14 @@ internal class ArrowResponseECallAdapter( private val errorConverter: Converter = retrofit.responseBodyConverter(errorType, arrayOfNulls(0)) - override fun adapt(call: Call): Call> = ResponseECall(call, errorConverter) + override fun adapt(call: Call): Call> = ResponseECall(call, errorConverter, bodyType) override fun responseType(): Type = bodyType class ResponseECall( private val original: Call, - private val errorConverter: Converter + private val errorConverter: Converter, + private val bodyType: Type ) : Call> { override fun enqueue(callback: Callback>) { @@ -44,13 +44,13 @@ internal class ArrowResponseECallAdapter( callback, this@ResponseECall, errorConverter, + bodyType, response, { body, responseT -> - val bodyE: Either = body.right() - Response.success(responseT.code(), ResponseE(responseT.raw(), bodyE)) + Response.success(responseT.code(), ResponseE(responseT.raw(), body.right())) }, { errorBody, responseV -> - Response.success>(ResponseE(responseV.raw(), errorBody.left())) + Response.success(ResponseE(responseV.raw(), errorBody.left())) } ) } @@ -61,7 +61,7 @@ internal class ArrowResponseECallAdapter( override fun timeout(): Timeout = original.timeout() - override fun clone(): Call> = ResponseECall(original.clone(), errorConverter) + override fun clone(): Call> = ResponseECall(original.clone(), errorConverter, bodyType) override fun isCanceled(): Boolean = original.isCanceled diff --git a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/EitherCallAdapterFactory.kt b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/EitherCallAdapterFactory.kt index 16befc18d91..edc347c5de4 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/EitherCallAdapterFactory.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/EitherCallAdapterFactory.kt @@ -56,7 +56,10 @@ public class EitherCallAdapterFactory : CallAdapter.Factory() { } } - private fun eitherAdapter(returnType: ParameterizedType, retrofit: Retrofit): CallAdapter>? { + private fun eitherAdapter( + returnType: ParameterizedType, + retrofit: Retrofit + ): CallAdapter>? { val wrapperType = getParameterUpperBound(0, returnType) return when (getRawType(wrapperType)) { Either::class.java -> { diff --git a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/eitherAdapter.kt b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/eitherAdapter.kt index 6965c06b587..79c20e4dd1b 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/eitherAdapter.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/main/kotlin/arrow/retrofit/adapter/either/eitherAdapter.kt @@ -5,11 +5,13 @@ import retrofit2.Call import retrofit2.Callback import retrofit2.Converter import retrofit2.Response +import java.lang.reflect.Type internal inline fun onResponseFn( callback: Callback, call: Call, errorConverter: Converter, + bodyType: Type, response: Response, newResponseFn: (R, Response) -> Response, errorResponseFn: (E, Response) -> Response @@ -17,7 +19,14 @@ internal inline fun onResponseFn( 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)) } diff --git a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapterTest.kt b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapterTest.kt index a1c16bd47c5..61342550cf9 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapterTest.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowEitherCallAdapterTest.kt @@ -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)) diff --git a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowResponseEAdapterTest.kt b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowResponseEAdapterTest.kt index 36f45a90995..da0cbb273a4 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowResponseEAdapterTest.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/ArrowResponseEAdapterTest.kt @@ -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)) diff --git a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/retrofit/SuspendApiTestClient.kt b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/retrofit/SuspendApiTestClient.kt index f44336b4ae8..da50459e83f 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/retrofit/SuspendApiTestClient.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/retrofit/SuspendApiTestClient.kt @@ -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 + @POST("/") + suspend fun postSomething(@Body something: String): Either + @GET("/") suspend fun getResponseE(): ResponseE + + @POST("/") + suspend fun postSomethingResponseE(@Body something: String): ResponseE }