Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 55 additions & 15 deletions src/main/kotlin/com/haroldadmin/cnradapter/NetworkResponse.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import java.io.IOException
* - Internet connectivity problems, the [NetworkResponse] is [NetworkResponse.NetworkError]
* - Any other problems (e.g. Serialization errors), the [NetworkResponse] is [NetworkResponse.UnknownError].
*/
public sealed interface NetworkResponse<S, E> {
public sealed interface NetworkResponse<out S, out E> {

public sealed interface Success<S> : NetworkResponse<S, Nothing> {
public val body: S
public val response: Response<*>
}

/**
* The result of a successful network request.
*
Expand All @@ -35,10 +41,43 @@ public sealed interface NetworkResponse<S, E> {
* @param body The parsed body of the successful response.
* @param response The original [Response] from Retrofit
*/
public data class Success<S, E>(
public val body: S,
public val response: Response<*>
) : NetworkResponse<S, E> {
public data class OK<S>(
public override val body: S,
public override val response: Response<*>
) : Success<S> {
/**
* The status code returned by the server.
*
* Alias for [Response.code] of the original response
*/
public val code: Int
get() = response.code()

/**
* The headers returned by the server.
*
* Alias for [Response.headers] of the original response
*/
public val headers: Headers
get() = response.headers()
}

/**
* The result of a successful network request.
*
* If you expect your server response to not contain a body, set the success body type ([S]) to [Unit].
* If you expect your server response to sometimes not contain a body (e.g. for response code 204), set
* [S] to [Unit] and deserialize the raw [response] manually when needed.
*
* @param response The original [Response] from Retrofit
*/
public data class NoContent(
public override val response: Response<*>
) : Success<Nothing> {

override val body: Nothing
get() = throw UnsupportedOperationException("Attempted to obtain body from no content response.")

/**
* The status code returned by the server.
*
Expand All @@ -63,7 +102,7 @@ public sealed interface NetworkResponse<S, E> {
* body ([ServerError]), or due to a connectivity error ([NetworkError]), or due to an unknown
* error ([UnknownError]).
*/
public sealed interface Error<S, E> : NetworkResponse<S, E> {
public sealed interface Error<out E> : NetworkResponse<Nothing, E> {
/**
* The body of the failed network request, if available.
*/
Expand All @@ -81,10 +120,10 @@ public sealed interface NetworkResponse<S, E> {
* This result may or may not contain a [body], depending on the body
* supplied by the server.
*/
public data class ServerError<S, E>(
public data class ServerError<E>(
public override val body: E?,
public val response: Response<*>?,
) : Error<S, E> {
) : Error<E> {
/**
* The status code returned by the server.
*
Expand All @@ -108,28 +147,29 @@ public sealed interface NetworkResponse<S, E> {
/**
* The result of a network connectivity error
*/
public data class NetworkError<S, E>(
public data class NetworkError(
public override val error: IOException,
) : Error<S, E> {
) : Error<Nothing> {

/**
* Always `null` for a [NetworkError]
*/
override val body: E? = null
override val body: Nothing? = null
}

/**
* Result of an unknown error during a network request
* (e.g. Serialization errors)
*/
public data class UnknownError<S, E>(
public data class UnknownError(
public override val error: Throwable,
public val response: Response<*>?
) : Error<S, E> {
) : Error<Nothing> {

/**
* Always `null` for an [UnknownError]
*/
override val body: E? = null
override val body: Nothing? = null

/**
* The status code returned by the server.
Expand All @@ -152,4 +192,4 @@ public sealed interface NetworkResponse<S, E> {
*
* Useful for specifying return types of API calls that do not return a useful value.
*/
public typealias CompletableResponse<E> = NetworkResponse<Unit, E>
public typealias CompletableResponse<E> = NetworkResponse<Nothing, E>
23 changes: 10 additions & 13 deletions src/main/kotlin/com/haroldadmin/cnradapter/ResponseParser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal fun <S, E> Response<S>.asNetworkResponse(
return if (!isSuccessful) {
parseUnsuccessfulResponse(this, errorConverter)
} else {
parseSuccessfulResponse(this, successType)
parseSuccessfulResponse(this)
}
}

Expand All @@ -41,8 +41,9 @@ internal fun <S, E> Response<S>.asNetworkResponse(
private fun <S, E> parseUnsuccessfulResponse(
response: Response<S>,
errorConverter: Converter<ResponseBody, E>
): NetworkResponse.Error<S, E> {
val errorBody: ResponseBody = response.errorBody() ?: return NetworkResponse.ServerError(null, response)
): NetworkResponse.Error<E> {
val errorBody: ResponseBody =
response.errorBody() ?: return NetworkResponse.ServerError(null, response)

return try {
val convertedBody = errorConverter.convert(errorBody)
Expand All @@ -62,18 +63,14 @@ private fun <S, E> parseUnsuccessfulResponse(
* - Else return a [NetworkResponse.ServerError] with a null body
* - If response body is not null, return [NetworkResponse.Success] with the parsed body
*/
private fun <S, E> parseSuccessfulResponse(response: Response<S>, successType: Type): NetworkResponse<S, E> {
val responseBody: S? = response.body()
if (responseBody == null) {
if (successType === Unit::class.java) {
@Suppress("UNCHECKED_CAST")
return NetworkResponse.Success<Unit, E>(Unit, response) as NetworkResponse<S, E>
private fun <S, E> parseSuccessfulResponse(response: Response<S>): NetworkResponse<S, E> {
return when (val responseBody: S? = response.body()) {
null -> when (response.code()) {
204 -> NetworkResponse.NoContent(response)
else -> NetworkResponse.ServerError(null, response)
}

return NetworkResponse.ServerError(null, response)
else -> NetworkResponse.OK(responseBody, response)
}

return NetworkResponse.Success(responseBody, response)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.SocketPolicy
import retrofit2.*
import retrofit2.CallAdapter
import retrofit2.Retrofit
import retrofit2.http.GET
import java.io.IOException
import java.time.Duration

class DeferredNetworkResponseAdapterTest : DescribeSpec({
public class DeferredNetworkResponseAdapterTest : DescribeSpec({
describe(DeferredNetworkResponseAdapter::class.java.simpleName) {
it("should return success type correctly") {
val converterFactory = StringConverterFactory()
Expand Down Expand Up @@ -90,7 +91,7 @@ class DeferredNetworkResponseAdapterTest : DescribeSpec({
)

val response = service.getTextAsync().await()
response.shouldBeInstanceOf<NetworkResponse.Success<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.Success<String>>()
response.body shouldBe "Test Message"
}

Expand All @@ -103,20 +104,19 @@ class DeferredNetworkResponseAdapterTest : DescribeSpec({
)

val response = service.getTextAsync().await()
response.shouldBeInstanceOf<NetworkResponse.ServerError<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.ServerError<String>>()
response.body shouldBe "Not Found"
}

it("should handle 200 (with body) and 204 (no body) responses correctly") {
server.enqueue(MockResponse().setBody("Test Message").setResponseCode(200))
val response = service.getTextAsync().await()
response.shouldBeInstanceOf<NetworkResponse.Success<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.OK<String>>()
response.body shouldBe "Test Message"

server.enqueue(MockResponse().setResponseCode(204))
val noBodyResponse = service.getTextAsync().await()
noBodyResponse.shouldBeInstanceOf<NetworkResponse.ServerError<String, String>>()
noBodyResponse.body shouldBe null
noBodyResponse.shouldBeInstanceOf<NetworkResponse.NoContent>()
}

it("should return network error response as NetworkResponse.NetworkError") {
Expand All @@ -125,7 +125,7 @@ class DeferredNetworkResponseAdapterTest : DescribeSpec({
)

val response = service.getTextAsync().await()
response.shouldBeInstanceOf<NetworkResponse.NetworkError<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.NetworkError>()
response.error.shouldBeInstanceOf<IOException>()
}
}
Expand Down
14 changes: 7 additions & 7 deletions src/test/kotlin/com/haroldadmin/cnradapter/ExtensionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,28 @@ import retrofit2.http.GET
import java.io.IOException
import java.time.Duration

class ExtensionsTest : DescribeSpec({
public class ExtensionsTest : DescribeSpec({
context("Overloaded Invoke Operator") {
it("should return the underlying body for NetworkResponse.Success") {
val response = NetworkResponse.Success<String, String>("Test Message", Response.success("Test Message"))
val response = NetworkResponse.OK("Test Message", Response.success("Test Message"))
val body = response()
body shouldBe "Test Message"
}

it("should return null for NetworkResponse.ServerError") {
val response = NetworkResponse.ServerError<String, String>(null, null)
val response = NetworkResponse.ServerError(null, null)
val body = response()
body shouldBe null
}

it("should return null for NetworkResponse.NetworkError") {
val response = NetworkResponse.NetworkError<String, String>(IOException())
val response = NetworkResponse.NetworkError(IOException())
val body = response()
body shouldBe null
}

it("should return null for NetworkResponse.UnknownError") {
val response = NetworkResponse.UnknownError<String, String>(Exception(), null)
val response = NetworkResponse.UnknownError(Exception(), null)
val body = response()
body shouldBe null
}
Expand Down Expand Up @@ -77,7 +77,7 @@ class ExtensionsTest : DescribeSpec({
service.getTextAsync().await()
}

response.shouldBeInstanceOf<NetworkResponse.Success<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.Success<String>>()
response.body shouldBe "Hi!"
}

Expand All @@ -92,7 +92,7 @@ class ExtensionsTest : DescribeSpec({
service.getText()
}

response.shouldBeInstanceOf<NetworkResponse.Success<String, String>>()
response.shouldBeInstanceOf<NetworkResponse.Success<String>>()
response.body shouldBe "Hi!"
}
}
Expand Down
Loading