taler-rust

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

commit 196f5a77d9d8cc04099a7506f1c33dc2b3f92e73
parent 8ea13f1b751f0a4ee98dec9d4074942854c88690
Author: Antoine A <>
Date:   Thu, 23 Jan 2025 13:52:55 +0100

common: add config cmd

Diffstat:
MCargo.lock | 2++
MCargo.toml | 2++
Mcommon/taler-common/Cargo.toml | 2++
Acommon/taler-common/src/cli.rs | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/taler-common/src/config.rs | 198++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcommon/taler-common/src/lib.rs | 1+
Mwire-gateway/magnet-bank/Cargo.toml | 3+--
Mwire-gateway/magnet-bank/src/main.rs | 4++++
8 files changed, 190 insertions(+), 79 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2568,6 +2568,8 @@ dependencies = [ name = "taler-common" version = "0.1.0" dependencies = [ + "anyhow", + "clap", "criterion", "fastrand", "glob", diff --git a/Cargo.toml b/Cargo.toml @@ -29,3 +29,4 @@ tempfile = "3.15" taler-common = { path = "common/taler-common" } taler-api = { path = "common/taler-api" } test-utils = { path = "common/test-utils" } +anyhow = "1" +\ No newline at end of file diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml @@ -18,6 +18,8 @@ url.workspace = true thiserror.workspace = true fastrand.workspace = true tracing.workspace = true +clap.workspace = true +anyhow.workspace = true sqlx = { workspace = true, features = ["macros"] } [dev-dependencies] diff --git a/common/taler-common/src/cli.rs b/common/taler-common/src/cli.rs @@ -0,0 +1,57 @@ +/* + This file is part of TALER + Copyright (C) 2025 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 + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use crate::config::Config; + +/// Inspect the configuration +#[derive(clap::Subcommand, Debug)] +pub enum ConfigCmd { + /// Lookup config value + Get { + section: String, + option: String, + /// Interpret value as path with dollar-expansion + #[clap(short, long, default_value_t = false)] + filename: bool, + }, + /// Substitute variables in a path + Pathsub { path_expr: String }, + /// Dump the configuration + Dump, +} + +impl ConfigCmd { + pub fn run(&self, cfg: Config) -> anyhow::Result<()> { + let out = match self { + ConfigCmd::Get { + section, + option, + filename, + } => { + let sect = cfg.section(&section); + if *filename { + sect.path(option).require()? + } else { + sect.str(option).require()? + } + } + ConfigCmd::Pathsub { path_expr } => cfg.pathsub(&path_expr, 0)?, + ConfigCmd::Dump => cfg.to_string(), + }; + println!("{}", out); + Ok(()) + } +} diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -111,28 +111,28 @@ pub mod parser { } struct Parser { - cfg: IndexMap<String, IndexMap<String, String>>, + sections: IndexMap<String, IndexMap<String, String>>, + install_path: String, buf: String, } impl Parser { fn empty() -> Self { Self { - cfg: IndexMap::new(), + sections: IndexMap::new(), + install_path: String::new(), buf: String::new(), } } fn load_env(&mut self, src: ConfigSource) -> Result<(), ParserErr> { - let ConfigSource { - project_name, - exec_name, - .. - } = src; + let ConfigSource { project_name, .. } = src; // Load default path - let dir = - install_path(exec_name).map_err(|(p, e)| io_err("find installation path", p, e))?; + let dir = src + .install_path() + .map_err(|(p, e)| io_err("find installation path", p, e))?; + self.install_path = dir.to_string_lossy().into_owned(); let paths = IndexMap::from_iter( [ @@ -147,7 +147,7 @@ pub mod parser { ] .map(|(a, b)| (a.to_owned(), b.to_string_lossy().into_owned())), ); - self.cfg.insert("PATHS".to_owned(), paths); + self.sections.insert("PATHS".to_owned(), paths); // Load default configs let cfg_dir = dir.join("share").join(project_name).join("config.d"); @@ -250,8 +250,12 @@ 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 if let Some(secret_section) = tmp.sections.swap_remove(&section) + { + self.sections + .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)); } @@ -267,7 +271,8 @@ pub mod parser { } else if let Some(section) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { - current_section = Some(self.cfg.entry(section.to_uppercase()).or_default()); + current_section = + Some(self.sections.entry(section.to_uppercase()).or_default()); } else if let Some((name, value)) = line.split_once('=') { if let Some(current_section) = &mut current_section { // Trim whitespace @@ -294,20 +299,27 @@ pub mod parser { } Ok(()) } + + pub fn finish(self) -> Config { + Config { + sections: self.sections, + install_path: self.install_path, + } + } } /** Information about how the configuration is loaded */ #[derive(Debug, Clone, Copy)] pub struct ConfigSource { /** Name of the high-level project */ - project_name: &'static str, + pub project_name: &'static str, /** Name of the component within the package */ - component_name: &'static str, + pub component_name: &'static str, /** * Executable name that will be located on $PATH to * find the installation path of the package */ - exec_name: &'static str, + pub exec_name: &'static str, } impl ConfigSource { @@ -322,71 +334,70 @@ pub mod parser { exec_name, } } - } - /** - * Search the default configuration file path - * - * I will be the first existing file from this list: - * - $XDG_CONFIG_HOME/$componentName.conf - * - $HOME/.config/$componentName.conf - * - /etc/$componentName.conf - * - /etc/$projectName/$componentName.conf - * */ - fn default_config_path( - project_name: &str, - component_name: &str, - ) -> Result<Option<PathBuf>, (PathBuf, std::io::Error)> { - // TODO use a generator - let conf_name = format!("{component_name}.conf"); - - if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") { - let path = PathBuf::from(xdg).join(&conf_name); + /** + * Search the default configuration file path + * + * I will be the first existing file from this list: + * - $XDG_CONFIG_HOME/$componentName.conf + * - $HOME/.config/$componentName.conf + * - /etc/$componentName.conf + * - /etc/$projectName/$componentName.conf + * */ + fn default_config_path(&self) -> Result<Option<PathBuf>, (PathBuf, std::io::Error)> { + // TODO use a generator + let conf_name = format!("{}.conf", self.component_name); + + if let Some(xdg) = std::env::var_os("XDG_CONFIG_HOME") { + let path = PathBuf::from(xdg).join(&conf_name); + match path.try_exists() { + Ok(false) => {} + Ok(true) => return Ok(Some(path)), + Err(e) => return Err((path, e)), + } + } + + if let Some(home) = std::env::var_os("HOME") { + let path = PathBuf::from(home).join(".config").join(&conf_name); + match path.try_exists() { + Ok(false) => {} + Ok(true) => return Ok(Some(path)), + Err(e) => return Err((path, e)), + } + } + + let path = PathBuf::from("/etc").join(&conf_name); match path.try_exists() { Ok(false) => {} Ok(true) => return Ok(Some(path)), Err(e) => return Err((path, e)), } - } - if let Some(home) = std::env::var_os("HOME") { - let path = PathBuf::from(home).join(".config").join(&conf_name); + let path = PathBuf::from("/etc") + .join(self.project_name) + .join(&conf_name); match path.try_exists() { Ok(false) => {} Ok(true) => return Ok(Some(path)), Err(e) => return Err((path, e)), } - } - - let path = PathBuf::from("/etc").join(&conf_name); - match path.try_exists() { - Ok(false) => {} - Ok(true) => return Ok(Some(path)), - Err(e) => return Err((path, e)), - } - let path = PathBuf::from("/etc").join(project_name).join(&conf_name); - match path.try_exists() { - Ok(false) => {} - Ok(true) => return Ok(Some(path)), - Err(e) => return Err((path, e)), + Ok(None) } - Ok(None) - } - - /** Search for the binary installation path in PATH */ - fn install_path(exec_name: &str) -> Result<PathBuf, (PathBuf, std::io::Error)> { - let path_env = std::env::var("PATH").unwrap(); - for entry in path_env.split(':') { - let path = PathBuf::from(entry).join(exec_name); - if path.join(exec_name).exists() { - if let Some(parent) = path.parent() { - return parent.canonicalize().map_err(|e| (parent.to_path_buf(), e)); + /** Search for the binary installation path in PATH */ + fn install_path(&self) -> Result<PathBuf, (PathBuf, std::io::Error)> { + let path_env = std::env::var("PATH").unwrap(); + for entry in path_env.split(':') { + let path = PathBuf::from(entry).join(self.exec_name); + if path.join(self.exec_name).exists() { + if let Some(parent) = path.parent() { + return parent.canonicalize().map_err(|e| (parent.to_path_buf(), e)); + } } } + Ok(PathBuf::from("/usr")) } - Ok(PathBuf::from("/usr")) } impl Config { @@ -399,20 +410,21 @@ pub mod parser { 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) + if let Some(default) = src + .default_config_path() .map_err(|(p, e)| io_err("find defauld config path", p, e))? { parser.parse_file(&default, 0)?; } } } - Ok(Config(parser.cfg)) + Ok(parser.finish()) } 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)) + Ok(parser.finish()) } } } @@ -434,15 +446,31 @@ pub enum ValueErr { }, } +#[derive(Debug, thiserror::Error)] + +pub enum PathsubErr { + #[error("recursion limit in path substitution exceeded for '{0}'")] + Recursion(String), + #[error("unbalanced variable expression '{0}'")] + Unbalanced(String), + #[error("bad substitution '{0}'")] + Substitution(String), + #[error("unbound variable '{0}'")] + Unbound(String), +} + #[derive(Debug)] -pub struct Config(IndexMap<String, IndexMap<String, String>>); +pub struct Config { + sections: IndexMap<String, IndexMap<String, String>>, + install_path: String, +} impl Config { pub fn section<'cfg, 'arg>(&'cfg self, section: &'arg str) -> Section<'cfg, 'arg> { Section { section, config: self, - values: self.0.get(&section.to_uppercase()), + values: self.sections.get(&section.to_uppercase()), } } @@ -453,18 +481,20 @@ impl Config { * * This substitution is typically only done for paths. */ - fn pathsub(&self, str: &str, depth: u8) -> Result<String, String> { + pub fn pathsub(&self, str: &str, depth: u8) -> Result<String, PathsubErr> { if depth > 128 { - return Err(format!( - "recursion limit in path substitution exceeded for '{str}'" - )); + return Err(PathsubErr::Recursion(str.to_owned())); } else if !str.contains('$') { return Ok(str.to_owned()); } /** Lookup for variable value from PATHS section in the configuration and environment variables */ - fn lookup(cfg: &Config, name: &str, depth: u8) -> Option<Result<String, String>> { - if let Some(path_res) = cfg.0.get("PATHS").and_then(|section| section.get(name)) { + fn lookup(cfg: &Config, name: &str, depth: u8) -> Option<Result<String, PathsubErr>> { + if let Some(path_res) = cfg + .sections + .get("PATHS") + .and_then(|section| section.get(name)) + { return Some(cfg.pathsub(path_res, depth + 1)); } @@ -524,12 +554,12 @@ impl Config { false } }) else { - return Err(format!("unbalanced variable expression '{default}'")); + return Err(PathsubErr::Unbalanced(default.to_owned())); }; remaining = after_default; Some(default) } else { - return Err(format!("bad substitution '{after_name}'")); + return Err(PathsubErr::Substitution(after_name.to_owned())); }; if let Some(resolved) = lookup(self, name, depth + 1) { result.push_str(&resolved?); @@ -539,8 +569,22 @@ impl Config { result.push_str(&resolved); continue; } - return Err(format!("unbound variable '{name}'")); + return Err(PathsubErr::Unbound(name.to_owned())); + } + } +} + +impl Display for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "# install path: {}", self.install_path)?; + for (sect, values) in &self.sections { + writeln!(f, "[{sect}]")?; + for (key, value) in values { + writeln!(f, "{key} = {value}")?; + } + writeln!(f, "")?; } + Ok(()) } } diff --git a/common/taler-common/src/lib.rs b/common/taler-common/src/lib.rs @@ -17,6 +17,7 @@ pub mod api_common; pub mod api_params; pub mod api_wire; +pub mod cli; pub mod config; pub mod error_code; pub mod json_file; diff --git a/wire-gateway/magnet-bank/Cargo.toml b/wire-gateway/magnet-bank/Cargo.toml @@ -17,7 +17,6 @@ base64 = "0.22" form_urlencoded = "1.2" percent-encoding = "2.3" serde_urlencoded = "0.7.1" -anyhow = "1.0" passterm = "2.0" sqlx = { workspace = true, features = [ "postgres", @@ -35,7 +34,7 @@ thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true tokio.workspace = true - +anyhow.workspace = true [dev-dependencies] test-utils.workspace = true diff --git a/wire-gateway/magnet-bank/src/main.rs b/wire-gateway/magnet-bank/src/main.rs @@ -29,6 +29,7 @@ use magnet_bank::{ }; use sqlx::PgPool; use taler_common::{ + cli::ConfigCmd, config::{parser::ConfigSource, Config}, types::payto::{payto, Payto}, }; @@ -71,6 +72,8 @@ enum Command { // TODO account in config account: Payto, }, + #[command(subcommand)] + Config(ConfigCmd), /// Hidden dev commands #[command(subcommand, hide(true))] Dev(DevCmd), @@ -148,6 +151,7 @@ async fn app(args: Args) -> anyhow::Result<()> { }; worker.run().await?; } + Command::Config(cfg_cmd) => cfg_cmd.run(cfg)?, Command::Dev(dev_cmd) => dev::dev(cfg, dev_cmd).await?, } Ok(())