taler-rust

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

commit 0e445837bc785c9885e85812591ce7935943cc58
parent 1f583d2ee6e8b2aebdef2556ddd7d3aca80e52e7
Author: Antoine A <>
Date:   Tue, 14 Jan 2025 21:06:47 +0100

taler-common: improve config mapping ergonomic using macro

Diffstat:
MCargo.lock | 25+++++++++++++------------
Mcommon/taler-common/src/config.rs | 135++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mwire-gateway/magnet-bank/src/config.rs | 73+++++++++++++++++++++++++++++++++----------------------------------------
3 files changed, 138 insertions(+), 95 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -94,11 +94,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -195,9 +196,9 @@ dependencies = [ [[package]] name = "axum-test" -version = "17.0.1" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f1009889890a439cbf67a4071a2593d027c65209da4faeac5582f28ca9e6c3" +checksum = "fb67e8e9ef63a57d8f494ce291b92a215413ab9752a12bbf7de4969acb7b8cdd" dependencies = [ "anyhow", "assert-json-diff", @@ -1391,9 +1392,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c258647f65892e500c2478ef2c71ba008e7dc1774a8289345adbbb502a4def1" +checksum = "7597657ea66d53f6e926a67d4cc3d125c4b57fa662f2d007a5476307de948453" dependencies = [ "log", "portable-atomic", @@ -1476,9 +1477,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "magnet-bank" @@ -1559,9 +1560,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", ] @@ -2965,9 +2966,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" [[package]] name = "valuable" diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs @@ -14,7 +14,12 @@ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -use std::{fmt::Debug, fs::Permissions, os::unix::fs::PermissionsExt, str::FromStr}; +use std::{ + fmt::{Debug, Display}, + fs::Permissions, + os::unix::fs::PermissionsExt, + str::FromStr, +}; use indexmap::IndexMap; use url::Url; @@ -35,14 +40,14 @@ pub mod parser { use indexmap::IndexMap; use tracing::{trace, warn}; - use super::{Config, ValueError}; + use super::{Config, ValueErr}; #[derive(Debug, thiserror::Error)] pub enum ConfigErr { #[error("config error, {0}")] Parser(#[from] ParserErr), #[error("invalid config, {0}")] - Value(#[from] ValueError), + Value(#[from] ValueErr), } #[derive(Debug)] @@ -413,7 +418,7 @@ pub mod parser { } #[derive(Debug, thiserror::Error)] -pub enum ValueError { +pub enum ValueErr { #[error("Missing {ty} option '{option}' in section '{section}'")] Missing { ty: String, @@ -546,26 +551,46 @@ pub struct Section<'cfg, 'arg> { values: Option<&'cfg IndexMap<String, String>>, } +#[macro_export] +macro_rules! map_config { + ($self:expr, $ty:expr, $option:expr, $($key:expr => $parse:block),*$(,)?) => { + { + let keys = &[$($key,)*]; + $self.map($ty, $option, |value| { + match value { + $($key => { + (|| { + $parse + })().map_err(|e| ::taler_common::config::MapErr::Err(e)) + })*, + _ => Err(::taler_common::config::MapErr::Invalid(keys)) + } + }) + } + } +} + +pub use map_config; + +#[doc(hidden)] +pub enum MapErr { + Invalid(&'static [&'static str]), + Err(ValueErr), +} + impl<'cfg, 'arg> Section<'cfg, 'arg> { - /** Setup an accessor/converted for a [type] at [option] using [transform] */ - pub fn value<T: 'cfg, E: ToString>( + #[doc(hidden)] + fn inner<T>( &self, ty: &'arg str, option: &'arg str, - transform: impl FnOnce(&'cfg str) -> Result<T, E>, + transform: impl FnOnce(&'cfg str) -> Result<T, ValueErr>, ) -> Value<'arg, T> { let value = self .values .and_then(|m| m.get(&option.to_uppercase())) .filter(|it| !it.is_empty()) - .map(|raw| { - transform(raw).map_err(|e| ValueError::Invalid { - ty: ty.to_owned(), - section: self.section.to_owned(), - option: option.to_owned(), - err: e.to_string(), - }) - }) + .map(|raw| transform(raw)) .transpose(); Value { value, @@ -575,47 +600,71 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { } } - /** 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())) - } - - pub fn map<T: Copy>( + #[doc(hidden)] + pub fn map<T>( &self, ty: &'arg str, option: &'arg str, - map: &[(&str, T)], + transform: impl FnOnce(&'cfg str) -> Result<T, MapErr>, ) -> Value<'arg, T> { - self.value(ty, option, |value| { - map.iter() - .find_map(|(k, v)| (*k == value).then_some(*v)) - .ok_or_else(|| { + self.value(ty, option, |v| { + transform(v).map_err(|e| match e { + MapErr::Invalid(keys) => { let mut buf = "expected '".to_owned(); - match map { + match keys { [] => unreachable!("you must provide at least one mapping"), - [(unique, _)] => buf.push_str(unique), - [(first, _), other @ .., (second, _)] => { + [unique] => buf.push_str(unique), + [first, other @ .., last] => { buf.push_str(first); - for (k, _) in other { + for k in other { buf.push_str("', '"); buf.push_str(k); } buf.push_str("' or '"); - buf.push_str(second); + buf.push_str(last); } } buf.push_str("' got '"); - buf.push_str(value); + buf.push_str(v); buf.push('\''); - buf - }) + ValueErr::Invalid { + ty: ty.to_owned(), + section: self.section.to_owned(), + option: option.to_owned(), + err: buf, + } + } + MapErr::Err(e) => e, + }) + }) + } + + /** Setup an accessor/converted for a [type] at [option] using [transform] */ + pub fn value<T, E: 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 { + ty: ty.to_owned(), + section: self.section.to_owned(), + option: option.to_owned(), + err: e.to_string(), + }) }) } + /** 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 str(&self, option: &'arg str) -> Value<'arg, String> { self.value("string", option, |it| Ok::<_, &str>(it.to_owned())) @@ -683,25 +732,25 @@ impl<'cfg, 'arg> Section<'cfg, 'arg> { } pub struct Value<'arg, T> { - value: Result<Option<T>, ValueError>, + value: Result<Option<T>, ValueErr>, option: &'arg str, ty: &'arg str, section: &'arg str, } impl<T> Value<'_, T> { - pub fn opt(self) -> Result<Option<T>, ValueError> { + pub fn opt(self) -> Result<Option<T>, ValueErr> { self.value } /** Converted value of default if missing */ - pub fn default(self, default: T) -> Result<T, ValueError> { + pub fn default(self, default: T) -> Result<T, ValueErr> { Ok(self.value?.unwrap_or(default)) } /** Converted value or throw if missing */ - pub fn require(self) -> Result<T, ValueError> { - self.value?.ok_or_else(|| ValueError::Missing { + pub fn require(self) -> Result<T, ValueErr> { + self.value?.ok_or_else(|| ValueErr::Missing { ty: self.ty.to_owned(), section: self.section.to_owned(), option: self.option.to_owned(), diff --git a/wire-gateway/magnet-bank/src/config.rs b/wire-gateway/magnet-bank/src/config.rs @@ -20,7 +20,7 @@ use base64::{prelude::BASE64_STANDARD, Engine}; use reqwest::Url; use sqlx::postgres::PgConnectOptions; use taler_api::{auth::AuthMethod, Serve}; -use taler_common::config::{Config, ValueError}; +use taler_common::config::{map_config, Config, ValueErr}; use crate::magnet::Token; @@ -29,7 +29,7 @@ pub struct DbConfig { } impl DbConfig { - pub fn parse(cfg: &Config) -> Result<Self, ValueError> { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { let sect = cfg.section("magnet-bank-postgres"); Ok(Self { cfg: sect.postgres("CONFIG").require()?, @@ -43,46 +43,39 @@ pub struct WireGatewayConfig { } impl WireGatewayConfig { - pub fn parse(cfg: &Config) -> Result<Self, ValueError> { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { let sect = cfg.section("magnet-bank"); - let parse_tcp = || { - let port = sect.number("PORT").require()?; - let ip: IpAddr = sect.parse("IP addr", "BIND_TO").require()?; - Ok(Serve::Tcp(SocketAddr::new(ip, port))) - }; - let parse_unix = || { - let path = sect.path("UNIXPATH").require()?; - let permission = sect.unix_mode("UNIXPATH_MODE").require()?; - Ok(Serve::Unix { path, permission }) - }; - let serve = sect - .map::<&dyn Fn() -> Result<Serve, ValueError>>( - "serve", - "SERVE", - &[("tcp", &parse_tcp), ("unix", &parse_unix)], - ) - .require()?()?; + let serve = map_config!(sect, "serve", "SERVE", + "tcp" => { + let port = sect.number("PORT").require()?; + let ip: IpAddr = sect.parse("IP addr", "BIND_TO").require()?; + Ok::<Serve, ValueErr>(Serve::Tcp(SocketAddr::new(ip, port))) + }, + "unix" => { + let path = sect.path("UNIXPATH").require()?; + let permission = sect.unix_mode("UNIXPATH_MODE").require()?; + Ok::<Serve, ValueErr>(Serve::Unix { path, permission }) + } + ) + .require()?; + let auth = map_config!(sect, "auth_method", "AUTH_METHOD", + "none" => { + Ok(AuthMethod::None) + }, + "basic" => { + let username = sect.str("USERNAME").require()?; + let password = sect.str("PASSWORD").require()?; + Ok(AuthMethod::Basic( + BASE64_STANDARD.encode(format!("{username}:{password}")), + )) + }, + "bearer" => { + Ok(AuthMethod::Bearer(sect.str("AUTH_TOKEN").require()?)) + } + ) + .require()?; - let parse_basic = || { - let username = sect.str("USERNAME").require()?; - let password = sect.str("PASSWORD").require()?; - Ok(AuthMethod::Basic( - BASE64_STANDARD.encode(format!("{username}:{password}")), - )) - }; - let parse_bearer = || Ok(AuthMethod::Bearer(sect.str("AUTH_TOKEN").require()?)); - let auth = sect - .map::<&dyn Fn() -> Result<AuthMethod, ValueError>>( - "auth_method", - "AUTH_METHOD", - &[ - ("none", &|| Ok(AuthMethod::None)), - ("basic", &parse_basic), - ("bearer", &parse_bearer), - ], - ) - .require()?()?; Ok(Self { serve, auth }) } } @@ -94,7 +87,7 @@ pub struct MagnetConfig { } impl MagnetConfig { - pub fn parse(cfg: &Config) -> Result<Self, ValueError> { + pub fn parse(cfg: &Config) -> Result<Self, ValueErr> { let sect = cfg.section("magnet-bank"); Ok(Self { api_url: sect.parse("URL", "API_URL").require()?,