taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 14d99474e131e33f86cbe1ead7ce68bd9f33c07a
parent 8cb5cdb8d45cb095cbb277df52dd6d1daca1120e
Author: Antoine A <>
Date:   Thu, 17 Sep 2026 18:09:10 +0200

common: clean DD102 implementation

Diffstat:
Madapters/taler-cyclos/src/config.rs | 20++++++++++----------
Madapters/taler-magnet-bank/src/config.rs | 12++++++------
Madapters/taler-magnet-bank/src/setup.rs | 22++++++++++------------
Madapters/taler-magnet-bank/src/worker.rs | 7+------
Madapters/taler-wise/src/config.rs | 19++++++++-----------
Mcommon/taler-api/src/config.rs | 10+++++-----
Mcommon/taler-common/src/config.rs | 241+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcommon/taler-common/src/lib.rs | 24++++++++++++------------
Mtaler-apns-relay/src/apns.rs | 2+-
Mtaler-apns-relay/src/config.rs | 10+++++-----
10 files changed, 176 insertions(+), 191 deletions(-)

diff --git a/adapters/taler-cyclos/src/config.rs b/adapters/taler-cyclos/src/config.rs @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2025, 2026 Taler Systems SA + Copyright (C) 2025-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 @@ -22,7 +22,7 @@ use taler_api::{ config::{ApiCfg, DbCfg}, }; use taler_common::{ - config::{Config, ValueErr}, + config::{CfgErr, Config}, map_config, types::amount::Currency, }; @@ -36,11 +36,11 @@ pub enum AccountType { Normal, } -pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, ValueErr> { +pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> { DbCfg::parse(cfg.section("cyclosdb-postgres")) } -pub fn parse_account_payto(cfg: &Config, main: &MainCfg) -> Result<FullCyclosPayto, ValueErr> { +pub fn parse_account_payto(cfg: &Config, main: &MainCfg) -> Result<FullCyclosPayto, CfgErr> { let sect = cfg.section("cyclos"); let id = sect.parse("cyclos account id", "ACCOUNT_ID").require()?; let name = sect.str("NAME").require()?; @@ -62,7 +62,7 @@ pub struct MainCfg { } impl MainCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let sect = cfg.section("cyclos"); let url = sect.base_url("CYCLOS_URL").require()?; let root = format_compact!( @@ -86,7 +86,7 @@ pub struct HostCfg { } impl HostCfg { - pub fn parse(cfg: &Config, main: &MainCfg) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config, main: &MainCfg) -> Result<Self, CfgErr> { let sect = cfg.section("cyclos-worker"); Ok(Self { username: sect.str("USERNAME").require()?, @@ -108,7 +108,7 @@ pub struct ServeCfg { } impl ServeCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let main = MainCfg::parse(cfg)?; let payto = parse_account_payto(cfg, &main)?; @@ -144,7 +144,7 @@ pub struct SetupCfg { } impl SetupCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let main = MainCfg::parse(cfg)?; let main_s = cfg.section("cyclos"); let worker_s = cfg.section("cyclos-worker"); @@ -176,7 +176,7 @@ pub struct WorkerCfg { } impl WorkerCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let main = MainCfg::parse(cfg)?; let s = cfg.section("cyclos-worker"); Ok(Self { @@ -207,7 +207,7 @@ pub struct HarnessCfg { } impl HarnessCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let worker = WorkerCfg::parse(cfg)?; let s = cfg.section("cyclos-harness"); diff --git a/adapters/taler-magnet-bank/src/config.rs b/adapters/taler-magnet-bank/src/config.rs @@ -22,18 +22,18 @@ use taler_api::{ config::{ApiCfg, DbCfg}, }; use taler_common::{ - config::{Config, ValueErr}, + config::{CfgErr, Config}, map_config, }; use url::Url; use crate::{FullHuPayto, HuIban, magnet_api::oauth::Token}; -pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, ValueErr> { +pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> { DbCfg::parse(cfg.section("magnet-bankdb-postgres")) } -pub fn parse_account_payto(cfg: &Config) -> Result<FullHuPayto, ValueErr> { +pub fn parse_account_payto(cfg: &Config) -> Result<FullHuPayto, CfgErr> { let s = cfg.section("magnet-bank"); let iban: HuIban = s.parse("iban", "IBAN").require()?; let name = s.str("NAME").require()?; @@ -51,7 +51,7 @@ pub struct ServeCfg { } impl ServeCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let payto = parse_account_payto(cfg)?; let s = cfg.section("magnet-bank-httpd"); @@ -91,7 +91,7 @@ pub struct WorkerCfg { } impl WorkerCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let payto = parse_account_payto(cfg)?; let s = cfg.section("magnet-bank-worker"); Ok(Self { @@ -121,7 +121,7 @@ pub struct HarnessCfg { } impl HarnessCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let worker = WorkerCfg::parse(cfg)?; let s = cfg.section("magnet-bank-harness"); diff --git a/adapters/taler-magnet-bank/src/setup.rs b/adapters/taler-magnet-bank/src/setup.rs @@ -17,7 +17,7 @@ use std::io::ErrorKind; use aws_lc_rs::{encoding::AsBigEndian, signature::EcdsaKeyPair}; -use taler_common::{json_file, types::base32::Base32}; +use taler_common::{config::CfgErr, config_bail, json_file, types::base32::Base32}; use tracing::{info, warn}; use crate::{ @@ -42,21 +42,19 @@ pub struct Keys { pub signing_key: EcdsaKeyPair, } -pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { +pub fn load(cfg: &WorkerCfg) -> Result<Keys, CfgErr> { // Load JSON file let file: KeysFile = match json_file::load(&cfg.keys_path) { Ok(file) => file, - Err(e) => { - return Err(anyhow::anyhow!( - "Could not read magnet keys at '{}': {}", - cfg.keys_path, - e.kind() - )); - } + Err(e) => config_bail!( + "could not read magnet keys at '{}': {}", + cfg.keys_path, + e.kind() + ), }; - fn incomplete_err() -> anyhow::Error { - anyhow::anyhow!("Missing magnet keys, run 'taler-magnet-bank setup' first") + fn incomplete_err() -> CfgErr { + CfgErr::custom("missing magnet keys, run 'taler-magnet-bank setup' first") } // Check full @@ -64,7 +62,7 @@ pub fn load(cfg: &WorkerCfg) -> anyhow::Result<Keys> { let signing_key = file.signing_key.ok_or_else(incomplete_err)?; // Load signing key - let signing_key = parse_private_key(&signing_key)?; + let signing_key = parse_private_key(&signing_key).map_err(CfgErr::custom)?; Ok(Keys { access_token, diff --git a/adapters/taler-magnet-bank/src/worker.rs b/adapters/taler-magnet-bank/src/worker.rs @@ -77,12 +77,7 @@ pub async fn run_worker( transient: bool, ) -> anyhow::Result<()> { let cfg = WorkerCfg::parse(cfg)?; - let keys = setup::load(&cfg).map_err(|err| taler_common::config::ValueErr::Invalid { - ty: "keys file".into(), - section: "magnet-bank-worker".into(), - option: "KEYS_FILE".into(), - err: err.to_string(), - })?; + let keys = setup::load(&cfg)?; let client = AuthClient::new(client, &cfg.api_url, &cfg.consumer).upgrade(&keys.access_token); if transient { diff --git a/adapters/taler-wise/src/config.rs b/adapters/taler-wise/src/config.rs @@ -16,15 +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, ConfigErr, ValueErr}, - map_config, + config::{CfgErr, Config}, + config_bail, map_config, types::{ amount::Currency, payto::{ACH, BankID}, @@ -39,7 +38,7 @@ pub enum AccountType { Normal, } -pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, ValueErr> { +pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> { DbCfg::parse(cfg.section("wisedb-postgres")) } @@ -49,7 +48,7 @@ pub struct WiseBalance { pub payto: WiseAccount, } -fn balances(cfg: &Config) -> Result<Vec<WiseBalance>, ValueErr> { +fn balances(cfg: &Config) -> Result<Vec<WiseBalance>, CfgErr> { cfg.sections() .filter_map(|s| { (|| { @@ -91,7 +90,7 @@ pub struct SetupCfg { } impl SetupCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let s = cfg.section("wise-worker"); Ok(Self { profile_id: s.number("PROFILE_ID").opt()?, @@ -113,7 +112,7 @@ pub struct ServeCfg { } impl ServeCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let s = cfg.section("wise"); Ok(Self { name: s.cstr("NAME").require()?, @@ -135,7 +134,7 @@ pub struct WorkerCfg { } impl WorkerCfg { - pub fn parse(cfg: &Config) -> Result<Self, ConfigErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let s = cfg.section("wise-worker"); let cfg = Self { profile_id: s.number("PROFILE_ID").require()?, @@ -144,9 +143,7 @@ impl WorkerCfg { balances: balances(cfg)?, }; if cfg.balances.is_empty() { - Err(anyhow!( - "Missing configured balance in a section [wise-balance-*]" - ))?; + config_bail!("Missing configured balance in a section [wise-balance-*]"); } Ok(cfg) } diff --git a/common/taler-api/src/config.rs b/common/taler-api/src/config.rs @@ -18,7 +18,7 @@ use std::net::{IpAddr, SocketAddr}; use sqlx::postgres::PgConnectOptions; use taler_common::{ - config::{Section, ValueErr}, + config::{CfgErr, Section}, encoding::base64, map_config, }; @@ -33,7 +33,7 @@ pub struct DbCfg { } impl DbCfg { - pub fn parse(s: Section) -> Result<Self, ValueErr> { + pub fn parse(s: Section) -> Result<Self, CfgErr> { Ok(Self { cfg: s.postgres("CONFIG").require()?, sql_dir: s.path("SQL_DIR").require()?, @@ -48,7 +48,7 @@ pub enum AuthCfg { } impl AuthCfg { - pub fn parse(s: &Section) -> Result<Self, ValueErr> { + pub fn parse(s: &Section) -> Result<Self, CfgErr> { map_config!(s, "auth_method", "AUTH_METHOD", "none" => { AuthCfg::None }, "basic" => { @@ -85,7 +85,7 @@ pub struct ApiCfg { } impl ApiCfg { - pub fn parse(s: Section) -> Result<Option<Self>, ValueErr> { + pub fn parse(s: Section) -> Result<Option<Self>, CfgErr> { Ok(if s.boolean("ENABLED").require()? { Some(Self { auth: AuthCfg::parse(&s)?, @@ -97,7 +97,7 @@ impl ApiCfg { } impl Serve { - pub fn parse(s: &Section) -> Result<Self, ValueErr> { + pub fn parse(s: &Section) -> Result<Self, CfgErr> { map_config!(s, "serve", "SERVE", "tcp" => { let port = s.number("PORT").require()?; diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -15,14 +15,8 @@ */ use std::{ - borrow::Cow, - fmt::{Debug, Display}, - fs::Permissions, - os::unix::fs::PermissionsExt, - path::PathBuf, - str::FromStr, - sync::Arc, - time::Duration, + borrow::Cow, fs::Permissions, os::unix::fs::PermissionsExt, path::PathBuf, str::FromStr, + sync::Arc, time::Duration, }; use compact_str::CompactString; @@ -30,13 +24,10 @@ use indexmap::IndexMap; use jiff::{SignedDuration, Span}; use url::Url; -use crate::{ - config::parser::ParserErr, - types::{ - amount::{Amount, Currency}, - payto::PaytoURI, - validate_base_url, - }, +use crate::types::{ + amount::{Amount, Currency}, + payto::PaytoURI, + validate_base_url, }; pub mod parser { @@ -53,70 +44,29 @@ pub mod parser { use tracing::{trace, warn}; use super::Config; - use crate::config::{Inner, Line, Location, make_lowercase}; - - #[derive(Debug)] - - pub enum ParserErr { - IO(Cow<'static, str>, PathBuf, std::io::Error), - Line(Cow<'static, str>, PathBuf, usize, Option<String>), - } - - impl Display for ParserErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ParserErr::IO(action, path, err) => write!( - f, - "Could not {action} at '{}': {}", - path.to_string_lossy(), - err.kind() - ), - ParserErr::Line(msg, path, line, cause) => { - if let Some(cause) = cause { - write!(f, "{msg} at '{}:{line}': {cause}", path.to_string_lossy()) - } else { - write!(f, "{msg} at '{}:{line}'", path.to_string_lossy()) - } - } - } - } - } - - impl std::error::Error for ParserErr { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None - } - - fn description(&self) -> &str { - "description() is deprecated; use Display" - } - - fn cause(&self) -> Option<&dyn std::error::Error> { - self.source() - } - } + use crate::config::{CfgErr, Inner, Line, Location, make_lowercase}; fn io_err( action: impl Into<Cow<'static, str>>, path: impl Into<PathBuf>, err: std::io::Error, - ) -> ParserErr { - ParserErr::IO(action.into(), path.into(), err) + ) -> CfgErr { + CfgErr::IO(action.into(), path.into(), err) } fn line_err( msg: impl Into<Cow<'static, str>>, path: impl Into<PathBuf>, line: usize, - ) -> ParserErr { - ParserErr::Line(msg.into(), path.into(), line, None) + ) -> CfgErr { + CfgErr::Line(msg.into(), path.into(), line, None) } fn line_cause_err( msg: impl Into<Cow<'static, str>>, cause: impl Display, path: impl Into<PathBuf>, line: usize, - ) -> ParserErr { - ParserErr::Line(msg.into(), path.into(), line, Some(cause.to_string())) + ) -> CfgErr { + CfgErr::Line(msg.into(), path.into(), line, Some(cause.to_string())) } pub struct Parser { sections: IndexMap<String, IndexMap<String, Line>>, @@ -135,7 +85,7 @@ pub mod parser { } } - pub fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> { + pub fn load_env(&mut self, src: ConfigSource) -> Result<(), CfgErr> { let ConfigSource { project_name, .. } = src; // Load default path @@ -188,7 +138,7 @@ pub mod parser { Ok(()) } - pub fn parse_str(&mut self, str: &str) -> Result<(), ParserErr> { + pub fn parse_str(&mut self, str: &str) -> Result<(), CfgErr> { self.parse( std::io::Cursor::new(str), PathBuf::from_str("mem").unwrap(), @@ -196,7 +146,7 @@ pub mod parser { ) } - pub fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), ParserErr> { + pub fn parse_file(&mut self, src: PathBuf, depth: u8) -> Result<(), CfgErr> { trace!(target: "config", "load file at '{}'", src.to_string_lossy()); match std::fs::File::open(&src) { Ok(file) => self.parse(BufReader::new(file), src, depth + 1), @@ -209,7 +159,7 @@ pub mod parser { mut reader: B, src: PathBuf, depth: u8, - ) -> Result<(), ParserErr> { + ) -> Result<(), CfgErr> { let file = self.files.len(); self.files.push(src.clone()); let src = &src; @@ -235,7 +185,7 @@ pub mod parser { } else if let Some(directive) = l.strip_prefix("@") { // Parse directive let Some((name, arg)) = directive.split_once('@') else { - return Err(line_err(format!("Invalid directive line '{l}'"), src, line)); + return Err(line_err(format!("invalid directive line '{l}'"), src, line)); }; let arg = arg.trim_ascii_start(); // Exit current section @@ -246,7 +196,7 @@ pub mod parser { }; // Check recursion depth if depth > 128 { - return Err(line_err("Recursion limit in config inlining", src, line)); + return Err(line_err("recursion limit in config inlining", src, line)); } match make_lowercase(name).as_ref() { @@ -254,7 +204,7 @@ pub mod parser { "inline-matching" => { let paths = glob::glob(&parent.join(arg).to_string_lossy()).map_err(|e| { - line_cause_err("Malformed glob regex", e, src, line) + line_cause_err("malformed glob regex", e, src, line) })?; for path in paths { let path = @@ -265,7 +215,7 @@ pub mod parser { "inline-secret" => { let (section, secret_file) = arg.split_once(" ").ok_or_else(|| line_err( - "Invalid configuration, @inline-secret@ directive requires exactly two arguments", + "invalid configuration, @inline-secret@ directive requires exactly two arguments", src, line ) @@ -275,7 +225,7 @@ pub mod parser { let mut secret_cfg = Parser::empty(); if let Err(e) = secret_cfg.parse_file(parent.join(secret_file), depth) { - if let ParserErr::IO(_, path, err) = e { + if let CfgErr::IO(_, path, err) = e { warn!(target: "config", "{}", io_err(format!("read secret section [{section}]"), &path, err)) } else { return Err(e); @@ -288,12 +238,12 @@ pub mod parser { .or_default() .extend(secret_section); } else { - warn!(target: "config", "{}", line_err(format!("Configuration file at '{secret_file}' loaded with @inline-secret@ does not contain section [{section}]"), src, line)); + warn!(target: "config", "{}", line_err(format!("configuration file at '{secret_file}' loaded with @inline-secret@ does not contain section [{section}]"), src, line)); } } unknown => { return Err(line_err( - format!("Invalid directive '{unknown}'"), + format!("invalid directive '{unknown}'"), src, line, )); @@ -323,11 +273,11 @@ pub mod parser { }, ); } else { - return Err(line_err("Expected section header or directive", src, line)); + return Err(line_err("expected section header or directive", src, line)); } } else { return Err(line_err( - "Expected section header, option assignment or directive", + "expected section header, option assignment or directive", src, line, )); @@ -449,10 +399,7 @@ pub mod parser { impl Config { /// Load a config for a Taler component, optionally also load from a file. /// This is the standard way to load a Taler component config - pub fn load( - src: ConfigSource, - path: Option<impl Into<PathBuf>>, - ) -> Result<Config, ParserErr> { + pub fn load(src: ConfigSource, path: Option<impl Into<PathBuf>>) -> Result<Config, CfgErr> { let mut parser = Parser::empty(); parser.load_env(src)?; match path { @@ -470,14 +417,14 @@ pub mod parser { } /// Load config from an in memory string for testing - pub fn from_mem(str: &str) -> Result<Config, ParserErr> { + pub fn from_mem(str: &str) -> Result<Config, CfgErr> { let mut parser = Parser::empty(); parser.parse_str(str)?; Ok(parser.finish()) } /// Load config from an in memory string with env from a Taler component for testing - pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, ParserErr> { + pub fn from_mem_with_env(src: ConfigSource, str: &str) -> Result<Config, CfgErr> { let mut parser = Parser::empty(); parser.load_env(src)?; parser.parse_str(str)?; @@ -489,7 +436,7 @@ pub mod parser { src: ConfigSource, path: Option<impl Into<PathBuf>>, str: &str, - ) -> Result<Config, ParserErr> { + ) -> Result<Config, CfgErr> { let mut parser = Parser::empty(); parser.load_env(src)?; match path { @@ -511,31 +458,79 @@ 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), +#[macro_export] +macro_rules! config_bail { + ($msg:literal $(,)?) => { + return Err($crate::config::CfgErr::Custom(format!($msg))) + }; + ($err:expr $(,)?) => { + return Err($crate::config::CfgErr::Custom(format!($err))) + }; + ($fmt:expr, $($arg:tt)*) => { + return Err($crate::config::CfgErr::Custom(format!($fmt, $($arg)*))) + }; } -#[derive(Debug, thiserror::Error)] -pub enum ValueErr { - #[error("Missing {ty} option {option} in section [{section}]")] +/// DD102: will fail with exit code 6 and will not be restarted by systemd +#[derive(Debug)] +pub enum CfgErr { + IO(Cow<'static, str>, PathBuf, std::io::Error), + Line(Cow<'static, str>, PathBuf, usize, Option<String>), Missing { ty: String, section: String, option: String, }, - #[error("Invalid {ty} option {option} in section [{section}]: {err}")] Invalid { ty: String, section: String, option: String, err: String, }, + Custom(String), +} + +impl std::error::Error for CfgErr {} + +impl std::fmt::Display for CfgErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CfgErr::IO(action, path, err) => write!( + f, + "could not {action} at '{}': {}", + path.to_string_lossy(), + err.kind() + ), + CfgErr::Line(msg, path, line, cause) => { + if let Some(cause) = cause { + write!(f, "{msg} at '{}:{line}': {cause}", path.to_string_lossy()) + } else { + write!(f, "{msg} at '{}:{line}'", path.to_string_lossy()) + } + } + CfgErr::Missing { + ty, + section, + option, + } => write!(f, "missing {ty} option {option} in section [{section}]"), + CfgErr::Invalid { + ty, + section, + option, + err, + } => write!( + f, + "invalid {ty} option {option} in section [{section}]: {err}" + ), + CfgErr::Custom(e) => e.fmt(f), + } + } +} + +impl CfgErr { + pub fn custom(cause: impl std::fmt::Display) -> Self { + Self::Custom(cause.to_string()) + } } #[derive(Debug, thiserror::Error)] @@ -572,7 +567,7 @@ struct Inner { #[derive(Clone)] pub struct Config(Arc<Inner>); -impl Debug for Config { +impl std::fmt::Debug for Config { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } @@ -788,7 +783,7 @@ pub use map_config; #[doc(hidden)] pub enum MapErr { Invalid(&'static [&'static str]), - Err(ValueErr), + Err(CfgErr), } impl<'cfg, 'arg> Section<'cfg, 'arg> { @@ -797,7 +792,7 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { &self, ty: &'arg str, option: &'arg str, - transform: impl FnOnce(&'cfg str) -> Result<T, ValueErr>, + transform: impl FnOnce(&'cfg str) -> Result<T, CfgErr>, ) -> Value<'arg, T> { let value = self .values @@ -840,7 +835,7 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { buf.push_str("' got '"); buf.push_str(v); buf.push('\''); - ValueErr::Invalid { + CfgErr::Invalid { ty: ty.to_owned(), section: self.name.to_lowercase(), option: option.to_uppercase(), @@ -853,14 +848,14 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { } /** Setup an accessor/converted for a [type] at [option] using [transform] */ - pub fn value<T, E: Display>( + pub fn value<T, E: std::fmt::Display>( &self, ty: &'arg str, option: &'arg str, transform: impl FnOnce(&'cfg str) -> Result<T, E>, ) -> Value<'arg, T> { self.inner(ty, option, |v| { - transform(v).map_err(|e| ValueErr::Invalid { + transform(v).map_err(|e| CfgErr::Invalid { ty: ty.to_owned(), section: self.name.to_lowercase(), option: option.to_uppercase(), @@ -1023,25 +1018,25 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { } pub struct Value<'arg, T> { - value: Result<Option<T>, ValueErr>, + value: Result<Option<T>, CfgErr>, option: &'arg str, ty: &'arg str, section: &'arg str, } impl<T> Value<'_, T> { - pub fn opt(self) -> Result<Option<T>, ValueErr> { + pub fn opt(self) -> Result<Option<T>, CfgErr> { self.value } /** Converted value of default if missing */ - pub fn default(self, default: T) -> Result<T, ValueErr> { + pub fn default(self, default: T) -> Result<T, CfgErr> { Ok(self.value?.unwrap_or(default)) } /** Converted value or throw if missing */ - pub fn require(self) -> Result<T, ValueErr> { - self.value?.ok_or_else(|| ValueErr::Missing { + pub fn require(self) -> Result<T, CfgErr> { + self.value?.ok_or_else(|| CfgErr::Missing { ty: self.ty.to_owned(), section: self.section.to_lowercase(), option: self.option.to_uppercase(), @@ -1104,7 +1099,7 @@ mod test { let check_ok = || Config::load(SOURCE, Some(&config_path)).unwrap(); check_err(format!( - "Could not read config at '{config_path_fmt}': entity not found" + "could not read config at '{config_path_fmt}': entity not found" )); let config_file = std::fs::File::create_new(&config_path).unwrap(); @@ -1116,7 +1111,7 @@ mod test { return; } check_err(format!( - "Could not read config at '{config_path_fmt}': permission denied" + "could not read config at '{config_path_fmt}': permission denied" )); config_file @@ -1125,7 +1120,7 @@ mod test { check_ok(); std::fs::write(&config_path, "@inline@ test-second-conf.conf").unwrap(); check_err(format!( - "Could not read config at '{second_path_fmt}': entity not found" + "could not read config at '{second_path_fmt}': entity not found" )); let second_file = std::fs::File::create_new(&second_path).unwrap(); @@ -1133,7 +1128,7 @@ mod test { .set_permissions(Permissions::from_mode(0o222)) .unwrap(); check_err(format!( - "Could not read config at '{second_path_fmt}': permission denied" + "could not read config at '{second_path_fmt}': permission denied" )); std::fs::write(&config_path, "@inline-matching@[*").unwrap(); @@ -1141,7 +1136,7 @@ mod test { // (<tempdir>/[*), so the position moves with the length of $TMPDIR. check_err_loose( format!( - "Malformed glob regex at '{config_path_fmt}:1': Pattern syntax error near position " + "malformed glob regex at '{config_path_fmt}:1': Pattern syntax error near position " ), ": invalid range pattern", Config::load(SOURCE, Some(&config_path)), @@ -1149,16 +1144,16 @@ mod test { std::fs::write(&config_path, "@inline-matching@*second-conf.conf").unwrap(); check_err(format!( - "Could not read config at '{second_path_fmt}': permission denied" + "could not read config at '{second_path_fmt}': permission denied" )); std::fs::write(&config_path, "\n@inline-matching@*.conf").unwrap(); check_err(format!( - "Recursion limit in config inlining at '{config_path_fmt}:2'" + "recursion limit in config inlining at '{config_path_fmt}:2'" )); std::fs::write(&config_path, "\n\n@inline-matching@ *.conf").unwrap(); check_err(format!( - "Recursion limit in config inlining at '{config_path_fmt}:3'" + "recursion limit in config inlining at '{config_path_fmt}:3'" )); std::fs::write(&config_path, "@inline-secret@ secret test-second-conf.conf").unwrap(); @@ -1170,15 +1165,15 @@ mod test { let check = |err: &str, content: &str| check_err(err, Config::from_mem(content)); check( - "Expected section header, option assignment or directive at 'mem:1'", + "expected section header, option assignment or directive at 'mem:1'", "syntax error", ); check( - "Expected section header or directive at 'mem:1'", + "expected section header or directive at 'mem:1'", "key=value", ); check( - "Expected section header, option assignment or directive at 'mem:2'", + "expected section header, option assignment or directive at 'mem:2'", "[section]\nbad-line", ); @@ -1200,13 +1195,13 @@ mod test { // Missing section check_err( - "Missing string option VALUE in section [unknown]", + "missing string option VALUE in section [unknown]", cfg.section("unknown").str("value").require(), ); // Missing value check_err( - "Missing string option VALUE in section [section-a]", + "missing string option VALUE in section [section-a]", cfg.section("section-a").str("value").require(), ); } @@ -1228,7 +1223,7 @@ mod test { // Check missing msg let cfg = conf(""); check_err( - format!("Missing {ty} option VALUE in section [section]"), + format!("missing {ty} option VALUE in section [section]"), lambda(&cfg.section("section"), "value").require(), ); @@ -1250,7 +1245,7 @@ mod test { let cfg = conf(&format!("[section]\nvalue={raw}")); check_err( format!( - "Invalid {ty} option VALUE in section [section]: {}", + "invalid {ty} option VALUE in section [section]: {}", error_fmt(raw) ), lambda(&cfg.section("section"), "value").require(), @@ -1406,12 +1401,12 @@ mod test { let cfg = Config::from_mem("[section]\nvalue=tcp").unwrap(); check_err( - "Missing number option PORT in section [section]", + "missing number option PORT in section [section]", parse(&cfg.section("section"), "value").require(), ); check_err( - "Invalid mode option VALUE in section [section]: expected 'unix' got 'tcp'", + "invalid mode option VALUE in section [section]: expected 'unix' got 'tcp'", map_config!(&cfg.section("section"), "mode", "value", "unix" => { Mode::Unix }, ) diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -54,6 +54,11 @@ pub struct CommonArgs { verbose: bool, } +/// DD102: will fail with exit code 9 and will not be restarted by systemd +#[derive(Debug, thiserror::Error)] +#[error("{0}")] +pub struct PermanentErr(String); + pub fn taler_main( src: ConfigSource, args: CommonArgs, @@ -79,18 +84,13 @@ pub fn taler_main( if let Err(err) = result { error!(target: "cli", "{}", err); // 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 - }, - ) + std::process::exit(if err.chain().any(|cause| cause.is::<config::CfgErr>()) { + 6 + } else if err.chain().any(|cause| cause.is::<PermanentErr>()) { + 9 + } else { + 1 + }) } } diff --git a/taler-apns-relay/src/apns.rs b/taler-apns-relay/src/apns.rs @@ -192,7 +192,7 @@ impl Client { .install_default() .expect("failed to install the default TLS provider"); - let invalid_key = |message: String| taler_common::config::ValueErr::Invalid { + let invalid_key = |message: String| taler_common::config::CfgErr::Invalid { ty: "PKCS#8 private key file".into(), section: "apns-relay-worker".into(), option: "KEY_FILE".into(), diff --git a/taler-apns-relay/src/config.rs b/taler-apns-relay/src/config.rs @@ -18,9 +18,9 @@ use std::time::Duration; use compact_str::CompactString; use taler_api::{Serve, config::DbCfg}; -use taler_common::config::{Config, ValueErr}; +use taler_common::config::{CfgErr, Config}; -pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, ValueErr> { +pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> { DbCfg::parse(cfg.section("apns-relaydb-postgres")) } @@ -30,7 +30,7 @@ pub struct ServeCfg { } impl ServeCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let s = cfg.section("apns-relay-httpd"); let serve = Serve::parse(&s)?; @@ -47,7 +47,7 @@ pub struct ApnsConfig { } impl ApnsConfig { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let sect = cfg.section("apns-relay-worker"); Ok(Self { key_path: sect.path("key_file").require()?, @@ -65,7 +65,7 @@ pub struct WorkerCfg { } impl WorkerCfg { - pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { + pub fn parse(cfg: &Config) -> Result<Self, CfgErr> { let sect = cfg.section("apns-relay-worker"); Ok(Self { apns: ApnsConfig::parse(cfg)?,