use figment::providers::Env;
use figment::Figment;
#[cfg(feature = "dotenv")]
use std::path::Path;
use crate::error::{Error, ErrorKind};
use crate::source::LoadSpec;
pub(super) fn environment(prefix: &str, key: &str, nest: &str, allow_empty: bool) -> Env {
let mut env = Env::prefixed(prefix);
if !allow_empty {
let empty = empty_keys(prefix);
if !empty.is_empty() {
env = env.filter_map(move |name| {
let is_empty = empty
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(name.as_str()));
(!is_empty).then(|| name.into())
});
}
}
env.split(nest).profile(key)
}
const AMBIGUOUS: &[&str] = &["yes", "no", "on", "off", "null", "nil", "none"];
fn is_ambiguous(value: &str) -> bool {
let trimmed = value.trim();
AMBIGUOUS
.iter()
.any(|candidate| trimmed.eq_ignore_ascii_case(candidate))
}
fn ambiguous(variable: &str) -> Error {
Error::new(
ErrorKind::Env,
format!(
"`{variable}` is set to one of the ambiguous yes/no/on/off \
spellings, which `strict_env` refuses: it would arrive as a \
string, not a boolean; write `true`, `false`, or the value you \
mean"
),
)
}
pub(super) fn reject_ambiguous(prefix: &str) -> Result<(), Error> {
for (name, value) in std::env::vars_os() {
let Ok(name) = name.into_string() else {
continue;
};
let Ok(value) = value.into_string() else {
continue;
};
let matches_prefix = name
.get(..prefix.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix));
if matches_prefix && is_ambiguous(&value) {
return Err(ambiguous(&name));
}
}
Ok(())
}
fn empty_keys(prefix: &str) -> Vec<String> {
std::env::vars_os()
.filter_map(|(name, value)| {
let name = name.into_string().ok()?;
let value = value.into_string().unwrap_or_else(|_| "x".to_owned());
Some((name, value))
})
.filter(|(_, value)| value.trim().is_empty())
.filter_map(|(name, _)| {
name.get(..prefix.len())
.filter(|candidate| candidate.eq_ignore_ascii_case(prefix))
.map(|_| name[prefix.len()..].to_owned())
})
.collect()
}
#[cfg(feature = "dotenv")]
pub(super) fn merge_env_files(mut figment: Figment, spec: &LoadSpec<'_>) -> Result<Figment, Error> {
let Some(prefix) = spec.full_env_prefix() else {
return Ok(figment);
};
for file in spec.env_files {
let path = Path::new(file);
let entries = crate::dotenv::read(path)?;
if entries.is_empty() {
continue;
}
if spec.strict_env {
if let Some((name, _)) = entries.iter().find(|(_, value)| is_ambiguous(value)) {
return Err(ambiguous(name).with_origin(crate::Origin::File(path.to_owned())));
}
}
figment = figment.merge(crate::dotenv::DotenvProvider::new(
entries,
path,
&prefix,
spec.key,
spec.nest,
spec.allow_empty_env,
));
}
Ok(figment)
}
#[cfg(not(feature = "dotenv"))]
pub(super) fn merge_env_files(figment: Figment, spec: &LoadSpec<'_>) -> Result<Figment, Error> {
if spec.env_files.is_empty() {
return Ok(figment);
}
Err(Error::new(
ErrorKind::Backend,
"`.env` files need the `dotenv` feature; add features = [\"dotenv\"] \
to your dynamic-config dependency",
))
}