commit 196f5a77d9d8cc04099a7506f1c33dc2b3f92e73
parent 8ea13f1b751f0a4ee98dec9d4074942854c88690
Author: Antoine A <>
Date: Thu, 23 Jan 2025 13:52:55 +0100
common: add config cmd
Diffstat:
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(§ion);
+ 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(§ion) {
- self.cfg.entry(section).or_default().extend(secret_section);
+ } else if let Some(secret_section) = tmp.sections.swap_remove(§ion)
+ {
+ 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(§ion.to_uppercase()),
+ values: self.sections.get(§ion.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(())