use ::id_effect::{Effect, Needs, effect};
use crate::error::ConfigError;
use crate::provider::ConfigProviderKey;
pub trait WithConfigDefault<A, R>: Sized {
fn with_default(self, default: A) -> Effect<A, ConfigError, R>;
}
impl<A, R> WithConfigDefault<A, R> for Effect<A, ConfigError, R>
where
A: Clone + 'static,
R: 'static,
{
fn with_default(self, default: A) -> Effect<A, ConfigError, R> {
self.catch(move |e| match e {
ConfigError::Missing { .. } => ::id_effect::succeed(default.clone()),
other => ::id_effect::fail(other),
})
}
}
#[inline]
pub fn nested_path(namespace: &str, leaf: &[&str]) -> Vec<String> {
std::iter::once(namespace.to_string())
.chain(leaf.iter().map(|s| (*s).to_string()))
.collect()
}
pub fn read_string<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<String> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
Ok(Some(s)) => A::from(s),
}
})
}
pub fn read_string_opt<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<Option<String>> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(raw) => A::from(raw),
}
})
}
pub fn read_number<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<f64> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
let s = match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => {
return Err(E::from(ConfigError::Missing {
path: path_str.clone(),
}));
}
Ok(Some(s)) => s,
};
let n = s.parse::<f64>().map_err(|e| {
E::from(ConfigError::Invalid {
path: path_str,
value: s,
reason: e.to_string(),
})
})?;
A::from(n)
})
}
pub fn read_i64<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<i64> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
let s = match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => {
return Err(E::from(ConfigError::Missing {
path: path_str.clone(),
}));
}
Ok(Some(s)) => s,
};
let n = s.parse::<i64>().map_err(|e| {
E::from(ConfigError::Invalid {
path: path_str,
value: s,
reason: e.to_string(),
})
})?;
A::from(n)
})
}
pub fn read_bool<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<bool> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
let s = match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => {
return Err(E::from(ConfigError::Missing {
path: path_str.clone(),
}));
}
Ok(Some(s)) => s,
};
let b = match s.to_ascii_lowercase().as_str() {
"true" | "1" | "yes" => true,
"false" | "0" | "no" => false,
_ => {
return Err(E::from(ConfigError::Invalid {
path: path_str,
value: s,
reason: "expected boolean string".into(),
}));
}
};
A::from(b)
})
}
pub fn read_string_list<A, E, R>(path: &[&str]) -> Effect<A, E, R>
where
A: From<Vec<String>> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned: Vec<String> = path.iter().map(|s| s.to_string()).collect();
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
let s = match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
Ok(Some(s)) => s,
};
let delim = provider.0.seq_delim();
let list: Vec<String> = s
.split(delim)
.map(str::trim)
.filter(|x| !x.is_empty())
.map(str::to_string)
.collect();
A::from(list)
})
}
pub fn read_nested_string<A, E, R>(namespace: &str, leaf: &[&str]) -> Effect<A, E, R>
where
A: From<String> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned = nested_path(namespace, leaf);
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
Ok(Some(s)) => A::from(s),
}
})
}
pub fn read_nested_string_list<A, E, R>(namespace: &str, leaf: &[&str]) -> Effect<A, E, R>
where
A: From<Vec<String>> + 'static,
E: From<ConfigError> + 'static,
R: Needs<ConfigProviderKey> + 'static,
{
let path_owned = nested_path(namespace, leaf);
effect!(|r: &mut R| {
let provider = r.need();
let refs: Vec<&str> = path_owned.iter().map(String::as_str).collect();
let path_str = refs.join(".");
let s = match provider.0.load_raw(&refs) {
Err(e) => return Err(E::from(e)),
Ok(None) => return Err(E::from(ConfigError::Missing { path: path_str })),
Ok(Some(s)) => s,
};
let delim = provider.0.seq_delim();
let list: Vec<String> = s
.split(delim)
.map(str::trim)
.filter(|x| !x.is_empty())
.map(str::to_string)
.collect();
A::from(list)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ConfigEnv;
use crate::MapConfigProvider;
use crate::ProviderOptions;
use crate::config_env;
use ::id_effect::run_blocking;
fn env_map(pairs: &[(&str, &str)]) -> ConfigEnv {
config_env(MapConfigProvider::from_pairs(pairs.iter().copied()))
}
#[test]
fn nested_path_builds_segments() {
assert_eq!(
nested_path("SERVER", &["HOST", "PORT"]),
vec!["SERVER", "HOST", "PORT"]
);
}
#[test]
fn read_string_ok_and_missing() {
let v: String = run_blocking(
read_string::<String, ConfigError, _>(&["K"]),
env_map(&[("K", "hello")]),
)
.unwrap();
assert_eq!(v, "hello");
let err = run_blocking(
read_string::<String, ConfigError, _>(&["MISSING"]),
env_map(&[]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Missing { .. }));
}
#[test]
fn read_string_opt_some_and_none() {
let v: Option<String> = run_blocking(
read_string_opt::<Option<String>, ConfigError, _>(&["K"]),
env_map(&[("K", "x")]),
)
.unwrap();
assert_eq!(v, Some("x".into()));
let none: Option<String> = run_blocking(
read_string_opt::<Option<String>, ConfigError, _>(&["MISSING"]),
env_map(&[]),
)
.unwrap();
assert_eq!(none, None);
}
#[test]
fn read_number_ok_missing_invalid() {
let n: f64 = run_blocking(
read_number::<f64, ConfigError, _>(&["N"]),
env_map(&[("N", "3.5")]),
)
.unwrap();
assert!((n - 3.5).abs() < f64::EPSILON);
let err = run_blocking(
read_number::<f64, ConfigError, _>(&["MISSING"]),
env_map(&[]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Missing { .. }));
let err = run_blocking(
read_number::<f64, ConfigError, _>(&["N"]),
env_map(&[("N", "not-a-number")]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Invalid { .. }));
}
#[test]
fn read_i64_ok_missing_invalid() {
let n: i64 = run_blocking(
read_i64::<i64, ConfigError, _>(&["N"]),
env_map(&[("N", "-42")]),
)
.unwrap();
assert_eq!(n, -42);
let err =
run_blocking(read_i64::<i64, ConfigError, _>(&["MISSING"]), env_map(&[])).unwrap_err();
assert!(matches!(err, ConfigError::Missing { .. }));
let err = run_blocking(
read_i64::<i64, ConfigError, _>(&["N"]),
env_map(&[("N", "1.2")]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Invalid { .. }));
}
#[test]
fn read_bool_variants_and_invalid() {
for (raw, expected) in [
("true", true),
("TRUE", true),
("1", true),
("yes", true),
("false", false),
("0", false),
("no", false),
] {
let b: bool = run_blocking(
read_bool::<bool, ConfigError, _>(&["B"]),
env_map(&[("B", raw)]),
)
.unwrap();
assert_eq!(b, expected, "raw={raw}");
}
let err = run_blocking(
read_bool::<bool, ConfigError, _>(&["MISSING"]),
env_map(&[]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Missing { .. }));
let err = run_blocking(
read_bool::<bool, ConfigError, _>(&["B"]),
env_map(&[("B", "maybe")]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Invalid { .. }));
}
#[test]
fn read_string_list_splits_on_seq_delim() {
let mut m = std::collections::HashMap::new();
m.insert("TAGS".into(), "a, b ,c".into());
let p = MapConfigProvider::with_options(
m,
ProviderOptions {
path_delim: "_",
seq_delim: ",",
},
);
let env = config_env(p);
let tags: Vec<String> = run_blocking(
read_string_list::<Vec<String>, ConfigError, _>(&["TAGS"]),
env,
)
.unwrap();
assert_eq!(tags, vec!["a", "b", "c"]);
}
#[test]
fn read_nested_string_and_list() {
let v: String = run_blocking(
read_nested_string::<String, ConfigError, _>("SERVER", &["HOST"]),
env_map(&[("SERVER_HOST", "z")]),
)
.unwrap();
assert_eq!(v, "z");
let list: Vec<String> = run_blocking(
read_nested_string_list::<Vec<String>, ConfigError, _>("APP", &["IDS"]),
env_map(&[("APP_IDS", "1,2")]),
)
.unwrap();
assert_eq!(list, vec!["1", "2"]);
}
#[test]
fn read_with_empty_path_is_invalid() {
let err = run_blocking(read_string::<String, ConfigError, _>(&[]), env_map(&[])).unwrap_err();
assert!(matches!(err, ConfigError::Invalid { .. }));
}
#[test]
fn with_default_replaces_only_missing() {
let def: String = run_blocking(
read_string::<String, ConfigError, _>(&["K"]).with_default("default".into()),
env_map(&[]),
)
.unwrap();
assert_eq!(def, "default");
let v: String = run_blocking(
read_string::<String, ConfigError, _>(&["K"]).with_default("default".into()),
env_map(&[("K", "real")]),
)
.unwrap();
assert_eq!(v, "real");
let err = run_blocking(
read_number::<f64, ConfigError, _>(&["N"]).with_default(0.0),
env_map(&[("N", "bad")]),
)
.unwrap_err();
assert!(matches!(err, ConfigError::Invalid { .. }));
}
}