/* * Copyright (c) 2020 Proton Technologies AG * This file is part of Proton Technologies AG and ProtonCore. * * ProtonCore is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonCore is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonCore. If not, see . */ package me.proton.core.network.domain import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withTimeoutOrNull import me.proton.core.network.domain.exception.ApiConnectionException import me.proton.core.network.domain.humanverification.HumanVerificationAvailableMethods /** * Result of the safe API call. * * @param T value type of the successful call result. */ sealed class ApiResult { /** * Successful call result. * * @param T Value type. * @property value Value. */ class Success(val value: T) : ApiResult() { override val valueOrNull get() = value } /** * Base class for error result. * @param cause [Exception] exception that caused this error for debugging purposes. */ sealed class Error(val cause: Throwable? = Exception("Unknown error")) : ApiResult() { /** * HTTP error. * * @property httpCode HTTP code. * @property message HTTP message. * @property proton Proton-specific HTTP error data. */ open class Http( val httpCode: Int, val message: String, val proton: ProtonData? = null, cause: Throwable? = null ) : Error(cause) { override fun toString() = "${this::class.simpleName}: httpCode=$httpCode message=$message, proton=$proton cause=$cause" } // detekt warning here is fine, the human verification details is optional, and if present in the response it is // set later, thus var. data class ProtonData( val code: Int, val error: String, var humanVerification: HumanVerificationAvailableMethods? = null ) /** * 429 "Too Many Requests" * * @property retryAfterSeconds Number of seconds to hold all requests (network layer will * automatically fail requests that don't comply) */ class TooManyRequest(val retryAfterSeconds: Int, proton: ProtonData? = null) : Http( httpCode = HTTP_TOO_MANY_REQUESTS, message = "Too Many Requests", proton = proton ) /** * Parsing error. Should not normally happen. */ class Parse(cause: Throwable?) : Error(cause) /** * Base class for connection errors (no response available) * * @property potentialBlock [true] if our API might have been blocked. */ open class Connection( private val potentialBlock: Boolean = false, cause: Throwable? = null ) : Error(cause) { override val isPotentialBlocking get() = potentialBlock val path = if (cause is ApiConnectionException) cause.path else null val query = if (cause is ApiConnectionException) cause.query else null } /** * Connection timed out. * * @param potentialBlock [true] if our API might have been blocked. */ class Timeout(potentialBlock: Boolean, cause: Throwable? = null) : Connection(potentialBlock, cause) /** * Certificate verification failed. */ class Certificate(cause: Throwable) : Connection(true, cause) /** * No connectivity. */ class NoInternet(cause: Throwable? = null) : Connection(false, cause) } /** * Value for successful calls or `null`. */ open val valueOrNull: T? get() = null /** * Value for successful calls or throw wrapped error if exist. */ val valueOrThrow: T get() { throwIfError() return checkNotNull(valueOrNull) } /** * Returns the encapsulated [Throwable] exception if this instance is [Error] or `null` otherwise. */ val exceptionOrNull: Throwable? get() = if (this is Error) cause else null /** * [true] for failed calls potentially caused by blocking. */ open val isPotentialBlocking: Boolean get() = false /** * Throws exception if this instance is [Error]. */ fun throwIfError() { if (this is Error) doThrow() } companion object { const val HTTP_TOO_MANY_REQUESTS = 429 /** * Introduce timeout for given block returning [ApiResult]. * * @param T Value type for successful call. * @param timeoutMs Timeout in milliseconds. * @param block potentially long-running lambda producing [ApiResult]. * @return block [ApiResult] or [ApiResult.Error.Timeout] on timeout. */ suspend fun withTimeout(timeoutMs: Long, block: suspend CoroutineScope.() -> ApiResult) = withTimeoutOrNull(timeoutMs, block) ?: Error.Timeout(true, null) } } fun ApiResult.Error.doThrow() { throw ApiException(this) } open class ApiException(val error: ApiResult.Error) : Exception( if (error is ApiResult.Error.Http && error.proton?.error != null) error.proton.error else error.cause?.message, error.cause ) /** * Return true if [ApiException.error] is retryable (e.g. connection issue or http error 5XX). * * @see ApiResult.isRetryable */ fun ApiException.isRetryable() = error.isRetryable() /** * Return true if [ApiResult] is retryable (e.g. connection issue or http error 5XX). */ fun ApiResult.isRetryable(): Boolean = when (this) { is ApiResult.Success, is ApiResult.Error.Parse, is ApiResult.Error.Certificate, is ApiResult.Error.TooManyRequest -> false is ApiResult.Error.Connection -> true is ApiResult.Error.Http -> httpCode in 500..599 } /** * Performs the given [action] if this instance represents an [ApiResult.Error]. * Returns the original `Result` unchanged. */ inline fun ApiResult.onError( action: (value: ApiResult.Error) -> Unit ): ApiResult { if (this is ApiResult.Error) action(this) return this } /** * Performs the given [action] if this instance represents an [ApiResult.Success]. * Returns the original `Result` unchanged. */ inline fun ApiResult.onSuccess( action: (value: T) -> Unit ): ApiResult { if (this is ApiResult.Success) action(value) return this }