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:
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()?,