libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 3052c91ead4fba54f45849770586b8c9c5149af4
parent bc3dc29d4f84cd1cebccbce5e851f41061f04cbc
Author: Antoine A <>
Date:   Wed, 29 Nov 2023 16:07:53 +0000

Improve CLI exception handling

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 64++++++++++++++++++++++++++--------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 69++++++++++++++++++++++++---------------------------------------------
Mutil/src/main/kotlin/ConfigCli.kt | 16++++++++--------
3 files changed, 58 insertions(+), 91 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -76,29 +76,28 @@ sealed class ServerConfig { data class Tcp(val port: Int): ServerConfig() } -fun talerConfig(configPath: String?): TalerConfig = catchError { +fun talerConfig(configPath: String?): TalerConfig { val config = TalerConfig(BANK_CONFIG_SOURCE) config.load(configPath) - config + return config } -fun TalerConfig.loadDbConfig(): DatabaseConfig = catchError { - DatabaseConfig( +fun TalerConfig.loadDbConfig(): DatabaseConfig { + return DatabaseConfig( dbConnStr = requireString("libeufin-bankdb-postgres", "config"), sqlDir = requirePath("libeufin-bankdb-postgres", "sql_dir") ) } -fun TalerConfig.loadServerConfig(): ServerConfig = catchError { - val method = requireString("libeufin-bank", "serve") - when (method) { +fun TalerConfig.loadServerConfig(): ServerConfig { + return when (val method = requireString("libeufin-bank", "serve")) { "tcp" -> ServerConfig.Tcp(requireNumber("libeufin-bank", "port")) "unix" -> ServerConfig.Unix(requireString("libeufin-bank", "unixpath"), requireNumber("libeufin-bank", "unixpath_mode")) else -> throw Exception("Unknown server method '$method' expected 'tcp' or 'unix'") } } -fun TalerConfig.loadBankConfig(): BankConfig = catchError { +fun TalerConfig.loadBankConfig(): BankConfig { val regionalCurrency = requireString("libeufin-bank", "currency") var fiatCurrency: String? = null; var fiatCurrencySpec: CurrencySpecification? = null @@ -107,7 +106,7 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { fiatCurrency = requireString("libeufin-bank", "fiat_currency"); fiatCurrencySpec = currencySpecificationFor(fiatCurrency) } - BankConfig( + return BankConfig( regionalCurrency = regionalCurrency, regionalCurrencySpec = currencySpecificationFor(regionalCurrency), allowRegistration = lookupBoolean("libeufin-bank", "allow_registration") ?: false, @@ -128,14 +127,13 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { fun String.notEmptyOrNull(): String? = if (isEmpty()) null else this -fun TalerConfig.currencySpecificationFor(currency: String): CurrencySpecification = catchError { - sections.find { +fun TalerConfig.currencySpecificationFor(currency: String): CurrencySpecification + = sections.find { it.startsWith("CURRENCY-") && requireBoolean(it, "enabled") && requireString(it, "code") == currency }?.let { loadCurrencySpecification(it) } ?: throw TalerConfigError("missing currency specification for $currency") -} -private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpecification = catchError { - CurrencySpecification( +private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpecification { + return CurrencySpecification( name = requireString(section, "name"), num_fractional_input_digits = requireNumber(section, "fractional_input_digits"), num_fractional_normal_digits = requireNumber(section, "fractional_normal_digits"), @@ -144,8 +142,8 @@ private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpec ) } -private fun TalerConfig.amount(section: String, option: String, currency: String): TalerAmount? = catchError { - val amountStr = lookupString(section, option) ?: return@catchError null +private fun TalerConfig.amount(section: String, option: String, currency: String): TalerAmount? { + val amountStr = lookupString(section, option) ?: return null val amount = try { TalerAmount(amountStr) } catch (e: Exception) { @@ -157,42 +155,31 @@ private fun TalerConfig.amount(section: String, option: String, currency: String "expected amount for section $section, option $option, but currency is wrong (got ${amount.currency} expected $currency" ) } - amount + return amount } -private fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount = catchError { +private fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount = amount(section, option, currency) ?: throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") -} -private fun TalerConfig.decimalNumber(section: String, option: String): DecimalNumber? = catchError { - val numberStr = lookupString(section, option) ?: return@catchError null +private fun TalerConfig.decimalNumber(section: String, option: String): DecimalNumber? { + val numberStr = lookupString(section, option) ?: return null try { - DecimalNumber(numberStr) + return DecimalNumber(numberStr) } catch (e: Exception) { throw TalerConfigError("expected decimal number for section $section, option $option, but number is malformed") } } -private fun TalerConfig.requireDecimalNumber(section: String, option: String): DecimalNumber = catchError { - decimalNumber(section, option) ?: +private fun TalerConfig.requireDecimalNumber(section: String, option: String): DecimalNumber + = decimalNumber(section, option) ?: throw TalerConfigError("expected decimal number for section $section, option $option, but config value is empty") -} -private fun TalerConfig.RoundingMode(section: String, option: String): RoundingMode? = catchError { - val str = lookupString(section, option) ?: return@catchError null; +private fun TalerConfig.RoundingMode(section: String, option: String): RoundingMode? { + val str = lookupString(section, option) ?: return null; try { - RoundingMode.valueOf(str) + return RoundingMode.valueOf(str) } catch (e: Exception) { throw TalerConfigError("expected rouding mode for section $section, option $option, but $str is unknown") } -} - -private fun <R> catchError(lambda: () -> R): R { - try { - return lambda() - } catch (e: TalerConfigError) { - logger.error(e.message) - kotlin.system.exitProcess(1) - } -} +} +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -44,7 +44,6 @@ import java.util.zip.DataFormatException import java.util.zip.Inflater import java.sql.SQLException import java.io.File -import kotlin.system.exitProcess import kotlinx.coroutines.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.* @@ -228,7 +227,7 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = help = "reset database (DANGEROUS: All existing data is lost)" ).flag() - override fun run() { + override fun run() = cliCmd(logger){ val config = talerConfig(configFile) val cfg = config.loadDbConfig() if (requestReset) { @@ -243,13 +242,10 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = when (res) { AccountCreationResult.BonusBalanceInsufficient -> {} AccountCreationResult.LoginReuse -> {} - AccountCreationResult.PayToReuse -> { - logger.error("Failed to create admin's account") - exitProcess(1) - } - AccountCreationResult.Success -> { + AccountCreationResult.PayToReuse -> + throw Exception("Failed to create admin's account") + AccountCreationResult.Success -> logger.info("Admin's account created") - } } } } @@ -261,7 +257,7 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") help = "set the configuration file" ) - override fun run() { + override fun run() = cliCmd(logger) { val cfg = talerConfig(configFile) val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() @@ -272,25 +268,21 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") logger.info("Ensure exchange account exists") val info = db.account.bankInfo("exchange") if (info == null) { - logger.error("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") - exitProcess(1) + throw Exception("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") } else if (!info.isTalerExchange) { - logger.error("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled") - exitProcess(1) + throw Exception("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled") } logger.info("Ensure conversion is enabled") val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-setup.sql") if (!sqlProcedures.exists()) { - logger.error("Missing libeufin-conversion-setup.sql file") - exitProcess(1) + throw Exception("Missing libeufin-conversion-setup.sql file") } db.conn { it.execSQLUpdate(sqlProcedures.readText()) } } else { logger.info("Ensure conversion is disabled") val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-drop.sql") if (!sqlProcedures.exists()) { - logger.error("Missing libeufin-conversion-drop.sql file") - exitProcess(1) + throw Exception("Missing libeufin-conversion-drop.sql file") } db.conn { it.execSQLUpdate(sqlProcedures.readText()) } // Remove conversion info from the database ? @@ -303,10 +295,8 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") is ServerConfig.Tcp -> { port = serverCfg.port } - is ServerConfig.Unix -> { - logger.error("Can only serve libeufin-bank via TCP") - exitProcess(1) - } + is ServerConfig.Unix -> + throw Exception("Can only serve libeufin-bank via TCP") } } module { corebankWebApp(db, ctx) } @@ -323,7 +313,7 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { private val username by argument("username") private val password by argument("password") - override fun run() { + override fun run() = cliCmd(logger) { val cfg = talerConfig(configFile) val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() @@ -331,14 +321,11 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { runBlocking { val res = db.account.reconfigPassword(username, password, null) when (res) { - AccountPatchAuthResult.UnknownAccount -> { - logger.error("Password change for '$username' account failed: unknown account") - exitProcess(1) - } + AccountPatchAuthResult.UnknownAccount -> + throw Exception("Password change for '$username' account failed: unknown account") AccountPatchAuthResult.OldPasswordMismatch -> { /* Can never happen */ } - AccountPatchAuthResult.Success -> { + AccountPatchAuthResult.Success -> logger.info("Password change for '$username' account succeeded") - } } } } @@ -351,15 +338,14 @@ class CreateAccount : CliktCommand("Create an account", name = "create-account") ) private val json: RegisterAccountRequest by argument().convert { Json.decodeFromString<RegisterAccountRequest>(it) } - override fun run() { + override fun run() = cliCmd(logger) { val cfg = talerConfig(configFile) val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) runBlocking { if (reservedAccounts.contains(json.username)) { - logger.error("Username '${json.username}' is reserved") - exitProcess(1) + throw Exception("Username '${json.username}' is reserved") } val internalPayto = json.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri()) @@ -379,21 +365,14 @@ class CreateAccount : CliktCommand("Create an account", name = "create-account") ) when (result) { - AccountCreationResult.BonusBalanceInsufficient -> { - logger.error("Insufficient admin funds to grant bonus") - exitProcess(1) - } - AccountCreationResult.LoginReuse -> { - logger.error("Account username reuse '${json.username}'") - exitProcess(1) - } - AccountCreationResult.PayToReuse -> { - logger.error("Bank internalPayToUri reuse '${internalPayto.canonical}'") - exitProcess(1) - } - AccountCreationResult.Success -> { + AccountCreationResult.BonusBalanceInsufficient -> + throw Exception("Insufficient admin funds to grant bonus") + AccountCreationResult.LoginReuse -> + throw Exception("Account username reuse '${json.username}'") + AccountCreationResult.PayToReuse -> + throw Exception("Bank internalPayToUri reuse '${internalPayto.canonical}'") + AccountCreationResult.Success -> logger.info("Account '${json.username}' created") - } } } } diff --git a/util/src/main/kotlin/ConfigCli.kt b/util/src/main/kotlin/ConfigCli.kt @@ -33,19 +33,19 @@ import kotlin.system.exitProcess private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.ConfigCli") -private fun <R> catchError(lambda: () -> R): R { +fun cliCmd(logger: Logger, lambda: () -> Unit) { try { - return lambda() - } catch (e: TalerConfigError) { + lambda() + } catch (e: Exception) { logger.error(e.message) exitProcess(1) } } -private fun talerConfig(configSource: ConfigSource, configPath: String?): TalerConfig = catchError { +private fun talerConfig(configSource: ConfigSource, configPath: String?): TalerConfig { val config = TalerConfig(configSource) config.load(configPath) - config + return config } class CliConfigCmd(configSource: ConfigSource) : CliktCommand("Inspect or change the configuration", name = "config") { @@ -69,7 +69,7 @@ private class CliConfigGet(private val configSource: ConfigSource) : CliktComman private val optionName by argument() - override fun run() { + override fun run() = cliCmd(logger) { val config = talerConfig(configSource, configFile) if (isPath) { val res = config.lookupPath(sectionName, optionName) @@ -98,7 +98,7 @@ private class CliConfigPathsub(private val configSource: ConfigSource) : CliktCo ) private val pathExpr by argument() - override fun run() { + override fun run() = cliCmd(logger) { val config = talerConfig(configSource, configFile) println(config.pathsub(pathExpr)) } @@ -110,7 +110,7 @@ private class CliConfigDump(private val configSource: ConfigSource) : CliktComma help = "set the configuration file" ) - override fun run() { + override fun run() = cliCmd(logger) { val config = talerConfig(configSource, configFile) println("# install path: ${config.getInstallPath()}") config.load(this.configFile)