taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

Response.kt (6075B)


      1 /*
      2  * This file is part of GNU Taler
      3  * (C) 2020 Taler Systems S.A.
      4  *
      5  * GNU Taler is free software; you can redistribute it and/or modify it under the
      6  * terms of the GNU General Public License as published by the Free Software
      7  * Foundation; either version 3, or (at your option) any later version.
      8  *
      9  * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
     10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11  * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12  *
     13  * You should have received a copy of the GNU General Public License along with
     14  * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15  */
     16 
     17 package net.taler.merchantlib
     18 
     19 import io.ktor.client.plugins.ResponseException
     20 import io.ktor.client.statement.bodyAsText
     21 import java.io.IOException
     22 import java.net.ConnectException
     23 import java.net.SocketTimeoutException
     24 import java.net.UnknownHostException
     25 import java.nio.channels.UnresolvedAddressException
     26 import kotlinx.serialization.Serializable
     27 import kotlinx.serialization.json.Json
     28 
     29 class Response<out T> private constructor(
     30     private val value: Any?
     31 ) {
     32     companion object {
     33         private const val NETWORK_ERROR_MESSAGE =
     34             "Network error: check your internet connection and merchant URL."
     35 
     36         suspend fun <T> response(request: suspend () -> T): Response<T> {
     37             return try {
     38                 success(request())
     39             } catch (e: Throwable) {
     40                 println(e)
     41                 failure(e)
     42             }
     43         }
     44 
     45         fun <T> success(value: T): Response<T> =
     46             Response(value)
     47 
     48         fun <T> failure(e: Throwable): Response<T> =
     49             Response(Failure(e))
     50     }
     51 
     52     val isFailure: Boolean get() = value is Failure
     53 
     54     suspend fun handle(onFailure: ((String) -> Unit)? = null, onSuccess: ((T) -> Unit)? = null) {
     55         if (value is Failure) onFailure?.let { it(getFailureString(value)) }
     56         else onSuccess?.let {
     57             @Suppress("UNCHECKED_CAST")
     58             it(value as T)
     59         }
     60     }
     61 
     62     suspend fun handleSuspend(
     63         onFailure: ((String) -> Any)? = null,
     64         onSuccess: (suspend (T) -> Any)? = null
     65     ) {
     66         if (value is Failure) onFailure?.let { it(getFailureString(value)) }
     67         else onSuccess?.let {
     68             @Suppress("UNCHECKED_CAST")
     69             it(value as T)
     70         }
     71     }
     72 
     73     private suspend fun getFailureString(failure: Failure): String = when (val exception = failure.exception) {
     74         is ResponseException -> getExceptionString(exception)
     75         is UnknownHostException,
     76         is UnresolvedAddressException,
     77         is ConnectException,
     78         is SocketTimeoutException -> NETWORK_ERROR_MESSAGE
     79         is IOException -> exception.message?.takeIf(String::isNotBlank) ?: NETWORK_ERROR_MESSAGE
     80         else -> exception.message?.takeIf(String::isNotBlank) ?: exception.toString()
     81     }
     82 
     83     private suspend fun getExceptionString(e: ResponseException): String {
     84         val response = e.response
     85         val responseText = response.bodyAsText()
     86         parseInventoryAvailabilityError(response.status.value, responseText)?.let { return it }
     87         return try {
     88             val error = Json.decodeFromString<Error>(responseText)
     89             buildString {
     90                 append("Error")
     91                 error.code?.let {
     92                     append(' ')
     93                     append(it)
     94                 }
     95                 append(" (")
     96                 append(response.status.value)
     97                 append(")")
     98                 error.hint?.takeIf(String::isNotBlank)?.let {
     99                     append(": ")
    100                     append(it)
    101                 }
    102                 error.detail?.takeIf(String::isNotBlank)?.let {
    103                     append(" - ")
    104                     append(it)
    105                 }
    106             }
    107         } catch (ex: Exception) {
    108             fallbackStatusMessage(response.status.value)
    109         }
    110     }
    111 
    112     private fun parseInventoryAvailabilityError(statusCode: Int, body: String): String? {
    113         if (statusCode != 410) return null
    114         val error = runCatching {
    115             Json { ignoreUnknownKeys = true }
    116                 .decodeFromString<InventoryAvailabilityError>(body)
    117         }.getOrNull() ?: return null
    118         val productId = error.productId?.takeIf(String::isNotBlank) ?: return null
    119         val requested = error.unitRequestedQuantity
    120             ?.takeIf(String::isNotBlank)
    121             ?: error.requestedQuantity?.toString()
    122             ?: return null
    123         val available = error.unitAvailableQuantity
    124             ?.takeIf(String::isNotBlank)
    125             ?: error.availableQuantity?.toString()
    126             ?: return null
    127         return "Inventory stock unavailable for product $productId: " +
    128             "$requested requested, $available available."
    129     }
    130 
    131     private fun fallbackStatusMessage(statusCode: Int): String = when (statusCode) {
    132         400 -> "Bad request (400)"
    133         401 -> "Unauthorized (401)"
    134         403 -> "Forbidden (403)"
    135         404 -> "Not found (404): check the merchant URL and instance path."
    136         408 -> "Request timed out (408)"
    137         in 500..599 -> "Server error ($statusCode)"
    138         else -> "HTTP error ($statusCode)"
    139     }
    140 
    141     private class Failure(val exception: Throwable)
    142 
    143     @Serializable
    144     private class Error(
    145         val code: Int?,
    146         val hint: String?,
    147         val detail: String? = null,
    148     )
    149 
    150     @Serializable
    151     private class InventoryAvailabilityError(
    152         @kotlinx.serialization.SerialName("product_id")
    153         val productId: String? = null,
    154         @kotlinx.serialization.SerialName("requested_quantity")
    155         val requestedQuantity: Int? = null,
    156         @kotlinx.serialization.SerialName("unit_requested_quantity")
    157         val unitRequestedQuantity: String? = null,
    158         @kotlinx.serialization.SerialName("available_quantity")
    159         val availableQuantity: Int? = null,
    160         @kotlinx.serialization.SerialName("unit_available_quantity")
    161         val unitAvailableQuantity: String? = null,
    162     )
    163 }