taler-rust

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

commit 88fad287f9047d4ab097253f39bc44e82729a623
parent 517e6d3e02180b4f9a58fc74716fa96c96c9b9fb
Author: Antoine A <>
Date:   Tue,  7 Jan 2025 19:47:14 +0100

common: improve config parser

Diffstat:
Mcommon/taler-common/src/config.rs | 215++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
1 file changed, 126 insertions(+), 89 deletions(-)

diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -14,11 +14,15 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::fmt::Debug; +use std::{fmt::Debug, str::FromStr}; use indexmap::IndexMap; +use url::Url; -use crate::types::amount::{Amount, Currency}; +use crate::types::{ + amount::{Amount, Currency}, + payto::Payto, +}; pub mod parser { use std::{ @@ -31,7 +35,15 @@ pub mod parser { use indexmap::IndexMap; use tracing::{trace, warn}; - use super::Config; + use super::{Config, ValueError}; + + #[derive(Debug, thiserror::Error)] + pub enum ConfigErr { + #[error("config error, {0}")] + Parser(#[from] ParserErr), + #[error("invalid config, {0}")] + Value(#[from] ValueError), + } #[derive(Debug)] @@ -60,6 +72,20 @@ pub mod parser { } } + 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() + } + } + fn io_err(action: &'static str, path: impl Into<PathBuf>, err: std::io::Error) -> ParserErr { ParserErr::IO(action, path.into(), err) } @@ -81,25 +107,23 @@ pub mod parser { struct Parser { cfg: IndexMap<String, IndexMap<String, String>>, - src: ConfigSource, buf: String, } impl Parser { - pub fn new(src: ConfigSource) -> Self { + fn empty() -> Self { Self { cfg: IndexMap::new(), - src, buf: String::new(), } } - fn load_defaults(&mut self) -> Result<(), ParserErr> { + fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> { let ConfigSource { project_name, exec_name, .. - } = self.src; + } = src; // Load default path let dir = @@ -212,8 +236,7 @@ pub mod parser { let section = section.to_uppercase(); - let mut tmp = Parser::new(self.src); - tmp.load_defaults()?; + let mut tmp = Parser::empty(); tmp.parse_file(src, depth)?; if let Err(e) = tmp.parse_file(src, depth) { @@ -222,13 +245,10 @@ pub mod parser { } else { return Err(e); } + } else if let Some(secret_section) = tmp.cfg.swap_remove(&section) { + self.cfg.entry(section).or_default().extend(secret_section); } else { - let mut secret_cfg = tmp.finalize(); - if let Some(secret_section) = secret_cfg.0.swap_remove(&section) { - self.cfg.entry(section).or_default().extend(secret_section); - } else { - warn!("{}", line_err(format!("Configuration file at '{}' loaded with @inline-secret@ does not contain section '{section}' ", file), src, num)); - } + warn!("{}", line_err(format!("Configuration file at '{}' loaded with @inline-secret@ does not contain section '{section}' ", file), src, num)); } } unknown => { @@ -269,10 +289,6 @@ pub mod parser { } Ok(()) } - - fn finalize(self) -> Config { - Config(self.cfg) - } } /** Information about how the configuration is loaded */ @@ -301,31 +317,6 @@ pub mod parser { exec_name, } } - - pub fn parse_default(self) -> Result<Config, ParserErr> { - let mut parser = Parser::new(self); - parser.load_defaults()?; - if let Some(default) = default_config_path(self.project_name, self.component_name) - .map_err(|(p, e)| io_err("find defauld config path", p, e))? - { - parser.parse_file(&default, 0)?; - } - Ok(parser.finalize()) - } - - pub fn parse_file(self, src: impl AsRef<Path>) -> Result<Config, ParserErr> { - let mut parser = Parser::new(self); - parser.load_defaults()?; - parser.parse_file(src.as_ref(), 0)?; - Ok(parser.finalize()) - } - - pub fn parse_str(self, src: &str) -> Result<Config, ParserErr> { - let mut parser = Parser::new(self); - parser.load_defaults()?; - parser.parse(std::io::Cursor::new(src), "mem".as_ref(), 0)?; - Ok(parser.finalize()) - } } /** @@ -392,6 +383,33 @@ pub mod parser { } Ok(PathBuf::from("/usr")) } + + impl Config { + pub fn from_file( + src: ConfigSource, + path: Option<impl AsRef<Path>>, + ) -> Result<Config, ParserErr> { + let mut parser = Parser::empty(); + parser.load_env(src)?; + match path { + Some(path) => parser.parse_file(path.as_ref(), 0)?, + None => { + if let Some(default) = default_config_path(src.project_name, src.component_name) + .map_err(|(p, e)| io_err("find defauld config path", p, e))? + { + parser.parse_file(&default, 0)?; + } + } + } + Ok(Config(parser.cfg)) + } + + pub fn from_mem(str: &str) -> Result<Config, ParserErr> { + let mut parser = Parser::empty(); + parser.parse(std::io::Cursor::new(str), "mem".as_ref(), 0)?; + Ok(Config(parser.cfg)) + } + } } #[derive(Debug, thiserror::Error)] @@ -415,7 +433,7 @@ pub enum ValueError { pub struct Config(IndexMap<String, IndexMap<String, String>>); impl Config { - pub fn section<'a>(&'a self, section: &'a str) -> Section<'a> { + pub fn section<'cfg, 'arg>(&'cfg self, section: &'arg str) -> Section<'cfg, 'arg> { Section { section, config: self, @@ -522,20 +540,20 @@ impl Config { } /** Accessor/Converter for Taler-like configuration sections */ -pub struct Section<'a> { - section: &'a str, - config: &'a Config, - values: Option<&'a IndexMap<String, String>>, +pub struct Section<'cfg, 'arg> { + section: &'arg str, + config: &'cfg Config, + values: Option<&'cfg IndexMap<String, String>>, } -impl<'a> Section<'a> { +impl<'cfg, 'arg> Section<'cfg, 'arg> { /** Setup an accessor/converted for a [type] at [option] using [transform] */ - fn value<T, E: ToString>( - &'a self, - option: &'a str, - ty: &'a str, - transform: impl FnOnce(&'a str) -> Result<T, E>, - ) -> Value<'a, T> { + pub fn value<T: 'cfg, E: ToString>( + &self, + ty: &'arg str, + option: &'arg str, + transform: impl FnOnce(&'cfg str) -> Result<T, E>, + ) -> Value<'arg, T> { let value = self .values .and_then(|m| m.get(&option.to_uppercase())) @@ -557,27 +575,36 @@ impl<'a> Section<'a> { } } + /** Access [option] as a parsable type */ + pub fn parse<E: std::fmt::Display, T: FromStr<Err = E>>( + &self, + ty: &'arg str, + option: &'arg str, + ) -> Value<'arg, T> { + self.value(ty, option, |it| it.parse::<T>().map_err(|e| e.to_string())) + } + /** Access [option] as str */ - pub fn string(&'a self, option: &'a str) -> Value<'a, String> { - self.value(option, "string", |it| Ok::<_, &str>(it.to_owned())) + pub fn str(&self, option: &'arg str) -> Value<'arg, String> { + self.value("string", option, |it| Ok::<_, &str>(it.to_owned())) } /** Access [option] as path */ - pub fn path(&'a self, option: &'a str) -> Value<'a, String> { - self.value(option, "path", |it| self.config.pathsub(it, 0)) + pub fn path(&self, option: &'arg str) -> Value<'arg, String> { + self.value("path", option, |it| self.config.pathsub(it, 0)) } /** Access [option] as a number */ - pub fn number(&'a self, option: &'a str) -> Value<'a, u64> { - self.value(option, "number", |it| { - it.parse::<u64>() + pub fn number<T: FromStr>(&self, option: &'arg str) -> Value<'arg, T> { + self.value("number", option, |it| { + it.parse::<T>() .map_err(|_| format!("'{it}' not a valid number")) }) } /** Access [option] as Boolean */ - pub fn boolean(&'a self, option: &'a str) -> Value<'a, bool> { - self.value(option, "boolean", |it| match it.to_uppercase().as_str() { + pub fn boolean(&self, option: &'arg str) -> Value<'arg, bool> { + self.value("boolean", option, |it| match it.to_uppercase().as_str() { "YES" => Ok(true), "NO" => Ok(false), _ => Err(format!("expected 'YES' or 'NO' got '{it}'")), @@ -585,9 +612,9 @@ impl<'a> Section<'a> { } /** Access [option] as Amount */ - pub fn amount(&'a self, option: &'a str, currency: &'static str) -> Value<'a, Amount> { + pub fn amount(&self, option: &'arg str, currency: &str) -> Value<'arg, Amount> { let currency: Currency = currency.parse().unwrap(); - self.value(option, "amount", |it| { + self.value("amount", option, |it| { let amount = it.parse::<Amount>().map_err(|e| e.to_string())?; if amount.currency != currency { return Err(format!( @@ -598,13 +625,28 @@ impl<'a> Section<'a> { Ok(amount) }) } + + /** Access [option] as url */ + pub fn url(&self, option: &'arg str) -> Value<'arg, Url> { + self.parse("url", option) + } + + /** Access [option] as payto */ + pub fn payto(&self, option: &'arg str) -> Value<'arg, Payto> { + self.parse("payto", option) + } + + /** Access [option] as Postgres URI */ + pub fn postgres(&self, option: &'arg str) -> Value<'arg, sqlx::postgres::PgConnectOptions> { + self.parse("Postgres URI", option) + } } -pub struct Value<'a, T> { +pub struct Value<'arg, T> { value: Result<Option<T>, ValueError>, - option: &'a str, - ty: &'a str, - section: &'a str, + option: &'arg str, + ty: &'arg str, + section: &'arg str, } impl<T> Value<'_, T> { @@ -637,7 +679,7 @@ mod test { use crate::{config::parser::ConfigSource, types::amount}; - use super::{Section, Value}; + use super::{Config, Section, Value}; const SOURCE: ConfigSource = ConfigSource::new("test", "test", "test"); @@ -657,7 +699,7 @@ mod test { let config_path_fmt = config_path.to_string_lossy(); let second_path_fmt = second_path.to_string_lossy(); - let check = |err: String| check_err(err, SOURCE.parse_file(&config_path)); + let check = |err: String| check_err(err, Config::from_file(SOURCE, Some(&config_path))); check(format!( "Could not read config at '{config_path_fmt}': entity not found" @@ -709,7 +751,7 @@ mod test { #[test] fn parsing() { - let check = |err: &str, content: &str| check_err(err, SOURCE.parse_str(&content)); + let check = |err: &str, content: &str| check_err(err, Config::from_mem(&content)); check( "Expected section header, option assignment or directive at 'mem:1'", @@ -724,9 +766,8 @@ mod test { "[section]\nbad-line", ); - let cfg = SOURCE - .parse_str( - r#" + let cfg = Config::from_mem( + r#" [section-a] @@ -738,19 +779,19 @@ mod test { second_value = "test" "#, - ) - .unwrap(); + ) + .unwrap(); // Missing section check_err( "Missing string option 'value' in section 'unknown'", - cfg.section("unknown").string("value").require(), + cfg.section("unknown").str("value").require(), ); // Missing value check_err( "Missing string option 'value' in section 'section-a'", - cfg.section("section-a").string("value").require(), + cfg.section("section-a").str("value").require(), ); } @@ -758,15 +799,11 @@ mod test { fn routine<T: Debug + Eq>( ty: &str, - lambda: for<'a> fn(&'a Section<'a>, &'a str) -> Value<'a, T>, + lambda: for<'cfg, 'arg> fn(&Section<'cfg, 'arg>, &'arg str) -> Value<'arg, T>, wellformed: &[(&[&str], T)], malformed: &[(&[&str], fn(&str) -> String)], ) { - let conf = |content: &str| { - SOURCE - .parse_str(&format!("{DEFAULT_CONF}\n{content}")) - .unwrap() - }; + let conf = |content: &str| Config::from_mem(&format!("{DEFAULT_CONF}\n{content}")).unwrap(); // Check missing msg let cfg = conf(""); @@ -806,7 +843,7 @@ mod test { fn string() { routine( "string", - |sect, value| sect.string(value), + |sect, value| sect.str(value), &[ (&["1", "\"1\""], "1".to_owned()), (&["test", "\"test\""], "test".to_owned()),