use std::sync::Arc;
type Lookup = Arc<dyn Fn(&str) -> Option<String> + Send + Sync>;
#[derive(Clone)]
pub(crate) struct CredentialSource {
lookup: Lookup,
}
impl CredentialSource {
pub(crate) fn from_env() -> Self {
Self {
lookup: Arc::new(|name| std::env::var(name).ok()),
}
}
#[cfg(test)]
pub(crate) fn fixed<K, V, I>(pairs: I) -> Self
where
K: Into<String>,
V: Into<String>,
I: IntoIterator<Item = (K, V)>,
{
let table: std::collections::HashMap<String, String> = pairs
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
Self {
lookup: Arc::new(move |name| table.get(name).cloned()),
}
}
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self::fixed(Vec::<(String, String)>::new())
}
pub(crate) fn get(&self, name: &str) -> Option<String> {
(self.lookup)(name).filter(|v| !v.is_empty())
}
}
impl Default for CredentialSource {
fn default() -> Self {
Self::from_env()
}
}
impl std::fmt::Debug for CredentialSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("CredentialSource(..)")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn credential_source_answers_only_listed_names() {
let creds = CredentialSource::fixed([("TOKEN_A", "value-a")]);
assert_eq!(creds.get("TOKEN_A").as_deref(), Some("value-a"));
assert_eq!(creds.get("TOKEN_B"), None);
}
#[test]
fn credential_source_treats_empty_as_unset() {
let creds = CredentialSource::fixed([("TOKEN_A", "")]);
assert_eq!(
creds.get("TOKEN_A"),
None,
"an exported-but-empty value must read as unset"
);
}
#[test]
fn credential_source_empty_answers_nothing() {
assert_eq!(CredentialSource::empty().get("ANYTHING"), None);
}
#[test]
fn credential_source_from_env_reads_the_process_environment() {
let creds = CredentialSource::from_env();
assert_eq!(creds.get("PATH"), std::env::var("PATH").ok());
}
#[test]
fn credential_source_debug_hides_the_lookup() {
assert_eq!(
format!("{:?}", CredentialSource::fixed([("T", "secret")])),
"CredentialSource(..)"
);
}
}