Last active
April 26, 2020 21:29
-
-
Save maxcruz/a6166785a409854acd9f8ed72c33d616 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.example.poc | |
import arrow.core.Either | |
import arrow.core.flatMap | |
import com.mytaxi.driver.core.presentation.AbstractPresenter | |
import com.mytaxi.driver.core.presentation.PresenterView | |
import com.mytaxi.driver.threading.APPLICATION_MAIN | |
import kotlinx.coroutines.withContext | |
import okhttp3.Interceptor | |
import okhttp3.OkHttpClient | |
import okhttp3.Response | |
import okhttp3.mockwebserver.MockWebServer | |
import org.junit.After | |
import org.junit.Before | |
import org.junit.Test | |
import org.mockito.ArgumentCaptor | |
import org.mockito.Mockito.mock | |
import retrofit2.Converter | |
import retrofit2.Retrofit | |
import retrofit2.converter.gson.GsonConverterFactory | |
import retrofit2.http.GET | |
import retrofit2.http.Query | |
import java.io.IOException | |
import kotlin.test.assertEquals | |
// ----- Service Layer ----- // | |
open class ServiceClientException(override val cause: Exception? = null) : Exception(cause) | |
sealed class ServerException : ServiceClientException() { | |
object Internal : ServerException() | |
object NotImplemented : ServerException() | |
object BadGateway : ServerException() | |
object ServiceUnavailable : ServerException() | |
object GatewayTimeout : ServerException() | |
//... | |
} | |
// Do we need payload on some of those cases? | |
sealed class ClientException : ServiceClientException() { | |
object BadRequest : ClientException() | |
object Unauthorized : ClientException() | |
object PaymentRequired : ClientException() | |
object Forbidden : ClientException() | |
object NotFound : ClientException() | |
object RequestTimeout : ClientException() | |
//... | |
} | |
// Simple Interceptor | |
fun errorInterceptor(chain: Interceptor.Chain): Response = | |
try { | |
val response = chain.proceed(chain.request()) | |
if (!response.isSuccessful) { | |
when (response.code) { | |
// Server: we just can react with fallbacks, or show a service not available message | |
500 -> throw ServerException.Internal | |
501 -> throw ServerException.NotImplemented | |
502 -> throw ServerException.BadGateway | |
503 -> throw ServerException.ServiceUnavailable | |
504 -> throw ServerException.GatewayTimeout | |
// Client: the client request is wrong, we should do something | |
400 -> throw ClientException.BadRequest | |
401 -> throw ClientException.Unauthorized | |
402 -> throw ClientException.PaymentRequired | |
403 -> throw ClientException.Forbidden | |
404 -> throw ClientException.NotFound | |
408 -> throw ClientException.RequestTimeout | |
// This only hapens if the server broke our contract returning something that we don't expect | |
else -> throw ServiceClientException(IllegalStateException("The status code ${response.code} was received but not handled!")) | |
} | |
} | |
response | |
} catch (error: IOException) { | |
// This is going to be throw in connection errors (no internet, timeout, etc) | |
throw ServiceClientException(error) | |
} | |
// OkHttpClient with the interceptor | |
inline fun getHttpClient(crossinline errorHandler: (Interceptor.Chain) -> Response): OkHttpClient = | |
OkHttpClient.Builder() | |
.addInterceptor(errorHandler) | |
.build() | |
// Retrofit client | |
fun getRetrofitClient(baseUrl: String, httpClient: OkHttpClient, jsonConverter: Converter.Factory): Retrofit = | |
Retrofit.Builder() | |
.baseUrl(baseUrl) | |
.client(httpClient) | |
.addConverterFactory(jsonConverter) | |
.build() | |
// POC models | |
data class Planet(val name: String, val imageUrl: String) | |
data class SolarSystem(val name: String, val neighbors: List<Planet>) | |
// Service interface | |
interface GalaxyRetrofitService { | |
@GET("/planet/{name}") | |
suspend fun getPlanetByName(@Query("name") name: String): Planet | |
@GET("/solar-system/{name}") | |
suspend fun getSolarSystem(@Query("name") name: String): SolarSystem | |
} | |
/** | |
* NOTE: In the real life we are create API classes extending AbstractRetrofitApi | |
* for run the service request in the AbstractRetrofitApi.execute method to | |
* convert handle IOException, NetworkException and convert the result to Try. | |
* That class is not needed anymore with this approach | |
*/ | |
// ----- Repository Implementation ----- // | |
// Just some application wide exception that we need to handle | |
class SomethingWentWrongException(override val cause: Exception? = null) : Exception(cause) | |
abstract class BaseRepository { | |
inline fun <T> execute(block: () -> T): T = | |
try { | |
block() | |
} catch (error: ServiceClientException) { | |
if (error is ServerException.ServiceUnavailable) { | |
throw SomethingWentWrongException(error) | |
} else { | |
// Let it crash in other cases | |
throw error | |
} | |
} | |
} | |
// Specific module errors | |
sealed class PlanetException : Exception() { | |
object WrongSpaceObjectException : PlanetException() | |
object OuterPlanetException : PlanetException() | |
} | |
sealed class SolarSystemException : Exception() { | |
object FarAwaySolarSystemException : SolarSystemException() | |
object UnknownGalaxy : SolarSystemException() | |
} | |
// Specific repository implementation | |
interface GalaxyRepository { | |
suspend fun getPlanet(name: String): Planet | |
suspend fun getSolarSystem(name: String): List<Planet> | |
} | |
class GalaxyServiceRepository(val service: GalaxyRetrofitService) : BaseRepository(), GalaxyRepository { | |
override suspend fun getPlanet(name: String): Planet = | |
execute { | |
try { | |
service.getPlanetByName(name) | |
// With a when I would need have the exhaustive evaluation or a else throwing something weird | |
} catch (_: ClientException.BadRequest) { | |
throw PlanetException.WrongSpaceObjectException | |
} catch (_: ClientException.NotFound) { | |
throw PlanetException.OuterPlanetException | |
} | |
} | |
override suspend fun getSolarSystem(name: String): List<Planet> = | |
execute { | |
try { | |
service.getSolarSystem(name).neighbors | |
} catch (_: ServerException.NotImplemented) { | |
throw SolarSystemException.UnknownGalaxy | |
} catch (_: ClientException.RequestTimeout) { | |
throw SolarSystemException.FarAwaySolarSystemException | |
} | |
} | |
} | |
// ----- Use case ----- // | |
suspend inline fun <reified L : Exception, R> Either.Companion.catchTyped(crossinline block: suspend () -> R): Either<L, R> = | |
catch { block() } | |
.mapLeft { | |
when(it) { | |
is L -> it | |
else -> throw it | |
} | |
} | |
class GalaxyUseCase(val repository: GalaxyRepository) { | |
suspend fun getJupiter(): Either<PlanetException, Planet> = Either | |
.catchTyped { repository.getPlanet("jupter") } | |
suspend fun getSaturn(): Either<PlanetException, Planet> = Either | |
// If this approach works for us we can improve it to compose mutliple catch specifing the type | |
.catchTyped<PlanetException, Planet> { repository.getPlanet("saturn") } | |
suspend fun getMilkyWayPlanets(): Either<SolarSystemException, List<Planet>> = Either | |
.catchTyped { repository.getSolarSystem("milkyway") } | |
} | |
// ----- Presenter ----- // | |
interface GalaxyView: PresenterView { | |
fun showError(message: String) | |
fun showPlanet(planet: Planet) | |
fun listGalaxyPlanets(list: List<Planet>) | |
} | |
// It's possible also wrap those dependencies to achieve pure functions | |
class SpacePresenter(val useCase: GalaxyUseCase): AbstractPresenter<GalaxyView>() { | |
fun getTheRingedPlanet() { | |
perform { | |
withContext(APPLICATION_MAIN) { | |
useCase.getSaturn() | |
.fold( | |
ifLeft = ::handlePlanetErrors, | |
ifRight = { view?.showPlanet(it) } | |
) | |
} | |
} | |
} | |
fun getTheBigPlanet() { | |
perform { | |
withContext(APPLICATION_MAIN) { | |
useCase.getJupiter() | |
.fold( | |
ifLeft = ::handlePlanetErrors, | |
ifRight = { view?.showPlanet(it) } | |
) | |
} | |
} | |
} | |
fun getMilkyWayGang() { | |
perform { | |
withContext(APPLICATION_MAIN) { | |
useCase.getMilkyWayPlanets() | |
.fold( | |
ifLeft = ::handleSolarSystemErrors, | |
ifRight = { view?.listGalaxyPlanets(it) } | |
) | |
} | |
} | |
} | |
// Issue: the error is a throwable here because there is no a Bifunctor IO type availble yet (comming in 0.11.0) | |
private fun handleSolarSystemErrors(error: SolarSystemException) { | |
when (error) { | |
is SolarSystemException.FarAwaySolarSystemException -> view?.showError("It's out of the observable space") | |
is SolarSystemException.UnknownGalaxy -> view?.showError("Not discovered yet") | |
} | |
} | |
private fun handlePlanetErrors(error: PlanetException) { | |
when (error) { | |
is PlanetException.WrongSpaceObjectException -> view?.showError("We didn't get that planet") | |
is PlanetException.OuterPlanetException -> view?.showError("It's taking so much time to get there") | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment