commit 88fad287f9047d4ab097253f39bc44e82729a623
parent 517e6d3e02180b4f9a58fc74716fa96c96c9b9fb
Author: Antoine A <>
Date: Tue, 7 Jan 2025 19:47:14 +0100
common: improve config parser
Diffstat:
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(§ion) {
+ 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(§ion) {
- 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()),