protoncore_android/network/domain/src/main/kotlin/me/proton/core/network/domain/ApiResult.kt

222 lines
6.9 KiB
Kotlin

/*
* 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 <https://www.gnu.org/licenses/>.
*/
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<out T> {
/**
* Successful call result.
*
* @param T Value type.
* @property value Value.
*/
class Success<T>(val value: T) : ApiResult<T>() {
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<Nothing>() {
/**
* 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 <T> withTimeout(timeoutMs: Long, block: suspend CoroutineScope.() -> ApiResult<T>) =
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 <T> ApiResult<T>.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 <T> ApiResult<T>.onError(
action: (value: ApiResult.Error) -> Unit
): ApiResult<T> {
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 <T> ApiResult<T>.onSuccess(
action: (value: T) -> Unit
): ApiResult<T> {
if (this is ApiResult.Success) action(value)
return this
}