commit 8cb5cdb8d45cb095cbb277df52dd6d1daca1120e
parent b2cc6de2b7551860d1c864df316c408df3708a14
Author: Antoine A <>
Date: Thu, 17 Sep 2026 17:21:17 +0200
common: add config err wrapper
Diffstat:
3 files changed, 39 insertions(+), 85 deletions(-)
diff --git a/adapters/taler-wise/src/config.rs b/adapters/taler-wise/src/config.rs
@@ -16,13 +16,14 @@
use std::time::Duration;
+use anyhow::anyhow;
use compact_str::CompactString;
use taler_api::{
Serve,
config::{ApiCfg, DbCfg},
};
use taler_common::{
- config::{Config, ValueErr},
+ config::{Config, ConfigErr, ValueErr},
map_config,
types::{
amount::Currency,
@@ -52,7 +53,7 @@ fn balances(cfg: &Config) -> Result<Vec<WiseBalance>, ValueErr> {
cfg.sections()
.filter_map(|s| {
(|| {
- Ok(if s.name.starts_with("wise-balance") {
+ Ok(if s.name.starts_with("wise-balance-") {
Some(WiseBalance {
id: s.number("id").require()?,
currency: s.currency("currency").require()?,
@@ -134,7 +135,7 @@ pub struct WorkerCfg {
}
impl WorkerCfg {
- pub fn parse(cfg: &Config) -> Result<Self, ValueErr> {
+ pub fn parse(cfg: &Config) -> Result<Self, ConfigErr> {
let s = cfg.section("wise-worker");
let cfg = Self {
profile_id: s.number("PROFILE_ID").require()?,
@@ -143,28 +144,10 @@ impl WorkerCfg {
balances: balances(cfg)?,
};
if cfg.balances.is_empty() {
- return Err(ValueErr::Missing {
- ty: "balance ID".into(),
- section: "wise-balance-*".into(),
- option: "ID".into(),
- });
+ Err(anyhow!(
+ "Missing configured balance in a section [wise-balance-*]"
+ ))?;
}
Ok(cfg)
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn worker_requires_a_balance() {
- let cfg =
- Config::from_mem("[wise-worker]\nPROFILE_ID=1\nTOKEN=test\nFREQUENCY=1 min\n").unwrap();
- assert!(matches!(
- WorkerCfg::parse(&cfg),
- Err(ValueErr::Missing { section, option, .. })
- if section == "wise-balance-*" && option == "ID"
- ));
- }
-}
diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs
@@ -30,10 +30,13 @@ use indexmap::IndexMap;
use jiff::{SignedDuration, Span};
use url::Url;
-use crate::types::{
- amount::{Amount, Currency},
- payto::PaytoURI,
- validate_base_url,
+use crate::{
+ config::parser::ParserErr,
+ types::{
+ amount::{Amount, Currency},
+ payto::PaytoURI,
+ validate_base_url,
+ },
};
pub mod parser {
@@ -49,17 +52,9 @@ pub mod parser {
use indexmap::IndexMap;
use tracing::{trace, warn};
- use super::{Config, ValueErr};
+ use super::Config;
use crate::config::{Inner, Line, Location, make_lowercase};
- #[derive(Debug, thiserror::Error)]
- pub enum ConfigErr {
- #[error("config error, {0}")]
- Parser(#[from] ParserErr),
- #[error("invalid config, {0}")]
- Value(#[from] ValueErr),
- }
-
#[derive(Debug)]
pub enum ParserErr {
@@ -517,6 +512,16 @@ pub mod parser {
}
#[derive(Debug, thiserror::Error)]
+pub enum ConfigErr {
+ #[error(transparent)]
+ Parser(#[from] ParserErr),
+ #[error(transparent)]
+ Value(#[from] ValueErr),
+ #[error(transparent)]
+ Custom(#[from] anyhow::Error),
+}
+
+#[derive(Debug, thiserror::Error)]
pub enum ValueErr {
#[error("Missing {ty} option {option} in section [{section}]")]
Missing {
@@ -534,7 +539,6 @@ pub enum ValueErr {
}
#[derive(Debug, thiserror::Error)]
-
pub enum PathsubErr {
#[error("recursion limit in path substitution exceeded for '{0}'")]
Recursion(String),
diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- Copyright (C) 2024, 2025, 2026 Taler Systems SA
+ Copyright (C) 2024-2026 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License as published by the Free Software
@@ -78,52 +78,19 @@ pub fn taler_main(
let result = runtime.block_on(app(&cfg));
if let Err(err) = result {
error!(target: "cli", "{}", err);
- std::process::exit(error_exit_status(&err));
- }
-}
-
-/// DD102: only diagnosed configuration errors suppress service recovery.
-fn error_exit_status(err: &anyhow::Error) -> i32 {
- if err.chain().any(|cause| {
- cause.is::<config::parser::ParserErr>()
- || cause.is::<config::ValueErr>()
- || cause.is::<config::PathsubErr>()
- }) {
- 6
- } else {
- 1
- }
-}
-
-#[cfg(test)]
-mod exit_status_tests {
- use super::*;
-
- #[test]
- fn configuration_errors_keep_their_status_through_context() {
- let cfg = Config::from_mem("[test]\nport = invalid\n").unwrap();
- let err = cfg
- .section("test")
- .number::<u16>("port")
- .require()
- .unwrap_err();
- assert_eq!(
- 6,
- error_exit_status(&anyhow::Error::new(err).context("starting server"))
- );
- let err = cfg.section("test").str("missing").require().unwrap_err();
- assert_eq!(6, error_exit_status(&anyhow::Error::new(err)));
- let err = Config::from_mem("not a configuration entry").unwrap_err();
- assert_eq!(6, error_exit_status(&anyhow::Error::new(err)));
- }
-
- #[test]
- fn unavailable_dependencies_remain_restartable() {
- let err = std::io::Error::from(std::io::ErrorKind::ConnectionRefused);
- assert_eq!(
- 1,
- error_exit_status(&anyhow::Error::new(err).context("database"))
- );
+ // DD102: only diagnosed configuration errors suppress service recovery
+ std::process::exit(
+ if err.chain().any(|cause| {
+ cause.is::<config::parser::ParserErr>()
+ || cause.is::<config::ValueErr>()
+ || cause.is::<config::PathsubErr>()
+ || cause.is::<config::ConfigErr>()
+ }) {
+ 6
+ } else {
+ 1
+ },
+ )
}
}