Skip to content

Instantly share code, notes, and snippets.

@maxcruz
Last active April 26, 2020 21:29
Show Gist options
  • Save maxcruz/a6166785a409854acd9f8ed72c33d616 to your computer and use it in GitHub Desktop.
Save maxcruz/a6166785a409854acd9f8ed72c33d616 to your computer and use it in GitHub Desktop.
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