Mapper¶
Sandwich provides versatile mapping extensions for ApiResponse
.
mapSuccess and suspendMapSuccess¶
If the ApiResponse
is of type ApiResponse.Success
, this function maps a T
type to a V
type within the ApiResponse
.
The provided example below illustrates the utilization of the mapSuccess
function to map the ApiResponse<UserAuthResponse>
type to ApiResponse<LoginInfo>
.
class LoginRepositoryImpl {
override fun requestToken(
authProvider: String,
authIdentifier: String,
email: String,
): Flow<ApiResponse<LoginInfo>> = flow {
val result = authService.requestToken(
UserRequest(
authProvider = authProvider,
authIdentifier = authIdentifier,
email = email,
),
).mapSuccess { LoginInfo(user = user, token = token) }
emit(result)
}.flowOn(ioDispatcher)
}
mapFailure and suspendMapFailure¶
In case the ApiResponse
is of type ApiResponse.Failure
, this operation maps a T
type from the ApiResponse
to a V
type.
It's useful when you need to manipulate the error response deliberately.
val apiResponse2 = apiResponse1.mapFailure { responseBody ->
"error body: ${responseBody?.string()}".toResponseBody()
}
Model Mapper¶
Mappers are especially useful when you need to transform the ApiResponse.Success
or ApiResponse.Failure.Error
into your custom model within the extension scopes of ApiResponse
.
ApiSuccessModelMapper¶
You can map the ApiResponse.Success
model to your custom model using the SuccessPosterMapper<T, R>
and the map
extension as demonstrated below:
object SuccessPosterMapper : ApiSuccessModelMapper<List<Poster>, Poster?> {
override fun map(apiSuccessResponse: ApiResponse.Success<List<Poster>>): Poster? {
return apiSuccessResponse.data.first()
}
}
// Maps the success response data.
val poster: Poster? = map(SuccessPosterMapper)
You can also use the map
extension with a lambda expression as shown below:
// Maps the success response data using a lambda.
map(SuccessPosterMapper) { poster ->
emit(poster) // You can use the `this` keyword instead of "poster".
}
If you want to receive transformed body data within the scope, you can utilize the mapper as a parameter with the onSuccess
or suspendOnSuccess
extensions, as illustrated below:
apiResponse.suspendOnSuccess(SuccessPosterMapper) {
val poster = this
}
ApiErrorModelMapper¶
You can utilize mappers to convert the ApiResponse.Failure.Error
model to your custom error model using the ApiErrorModelMapper<T>
and the map
extension, as demonstrated in the examples below:
// Define your custom error model.
data class ErrorEnvelope(
val code: Int,
val message: String
)
// Create a mapper for error responses.
// Within the `map` function, construct an instance of your custom model using the information from `ApiResponse.Failure.Error`.
object ErrorEnvelopeMapper : ApiErrorModelMapper<ErrorEnvelope> {
override fun map(apiErrorResponse: ApiResponse.Failure.Error<*>): ErrorEnvelope {
return ErrorEnvelope(apiErrorResponse.statusCode.code, apiErrorResponse.message())
}
}
// Apply the mapper to an error response.
response.onError {
// Use the mapper to convert ApiResponse.Failure.Error to your custom error model.
map(ErrorEnvelopeMapper) {
val code = this.code
val message = this.message
}
}
If you intend to obtain transformed data within the scope, you can use the mapper as a parameter with the onError
or suspendOnError
extensions, as shown in the examples below:
apiResponse.suspendOnError(ErrorEnvelopeMapper) {
val message = this.message
}
Global Failure Mapper¶
You can map ApiResponse.Failure
responses into your custom error response by using flatMap
as described ApiResponse.Failure.Error documentation.
Alternatively, Sandwich provides a robust solution for mapping responses, enabling you to transform all ApiResponse.Failure
responses into your preferred ApiResponse.Failure.Error
or ApiResponse.Failure.Exception
types. This can be applied globally without the need for using flatMap
extensions across all network and I/O requests or when creating ApiResponse
instances.
You can implement this globally by using SandwichInitializer.sandwichFailureMappers
. The example below shows how to map all your ApiResponse.Failure.Error
to your custom ApiResponse.Failure.Exception
.
data object UnKnownError : ApiResponse.Failure.Exception(
throwable = RuntimeException("unknwon error")
)
data object LimitedRequest : ApiResponse.Failure.Exception(
throwable = RuntimeException("your request is limited")
)
data object WrongArgument : ApiResponse.Failure.Exception(
throwable = RuntimeException("wrong argument")
)
data object HttpException : ApiResponse.Failure.Exception(
throwable = RuntimeException("http exception"),
)
SandwichInitializer.sandwichFailureMappers += listOf(
object : ApiResponseFailureMapper {
override fun map(apiResponse: ApiResponse.Failure<*>): ApiResponse.Failure<*> {
if (apiResponse is ApiResponse.Failure.Error) {
val errorBody = (apiResponse.payload as? okhttp3.Response)?.body?.string()
if (errorBody != null) {
val errorMessage: ErrorMessage = Json.decodeFromString(errorBody)
when (errorMessage.code) {
10000 -> LimitedRequest
10001 -> WrongArgument
10002 -> HttpException
else -> UnKnownError
}
}
}
return apiResponse
}
},
)
Given the example above, which maps all ApiResponse.Failure.Error
to your custom ApiResponse.Failure.Exception
according to your preferences, you'll only need to focus on handling exceptional cases when dealing with your ApiResponse
.
val apiResponse = service.fetchMovieList()
apiResponse.onSuccess {
// ..
}.onException {
when (this) {
LimitedRequest -> // ..
WrongArgument -> // ..
HttpException -> // ..
UnKnownError -> // ..
}
}