use std::env;
use std::fs;
use std::str::FromStr;
use super::{ConfigError, Result};
pub(crate) fn value(variable: &'static str) -> Result<Option<String>> {
let direct = read_variable(variable)?;
let file_variable = format!("{variable}_FILE");
let file = read_variable(&file_variable)?;
if direct.is_some() && file.is_some() {
return Err(ConfigError::VariableConflict { variable });
}
let resolved = match (direct, file) {
(Some(value), None) => Some(value),
(None, Some(path)) => Some(
fs::read_to_string(path)
.map_err(|source| ConfigError::VariableFile { variable, source })?,
),
(None, None) => None,
(Some(_), Some(_)) => unreachable!("conflicting sources were rejected"),
};
resolved
.map(|value| {
let value = value.trim();
if value.is_empty() {
Err(ConfigError::VariableValue {
variable,
message: "must not be empty",
})
} else {
Ok(value.to_owned())
}
})
.transpose()
}
pub(crate) fn parsed<T>(
variable: &'static str,
value: Option<String>,
expected: &'static str,
) -> Result<Option<T>>
where
T: FromStr,
{
value
.map(|value| {
value.parse::<T>().map_err(|_| ConfigError::VariableValue {
variable,
message: expected,
})
})
.transpose()
}
fn read_variable(variable: &str) -> Result<Option<String>> {
match env::var(variable) {
Ok(value) => Ok(Some(value)),
Err(env::VarError::NotPresent) => Ok(None),
Err(env::VarError::NotUnicode(_)) => Err(ConfigError::VariableUnicode {
variable: variable.to_owned(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsed_values_never_include_the_rejected_input() {
let error = parsed::<u16>(
"SMTP_PORT",
Some("not-a-port".to_owned()),
"must be an integer TCP port",
)
.expect_err("fixture must be invalid");
let rendered = format!("{error:?} {error}");
assert!(!rendered.contains("not-a-port"));
assert!(rendered.contains("SMTP_PORT"));
}
}