s3 0.1.36

A lean, modern, unofficial S3-compatible client for Rust.
Documentation
use crate::{Error, Result};

pub(crate) fn optional_var(name: &'static str) -> Result<Option<String>> {
    match std::env::var(name) {
        Ok(value) => Ok(Some(value)),
        Err(std::env::VarError::NotPresent) => Ok(None),
        Err(std::env::VarError::NotUnicode(_)) => Err(Error::invalid_config(format!(
            "{name} must be valid Unicode"
        ))),
    }
}

#[cfg(feature = "credentials-profile")]
pub(crate) fn optional_first_var(names: &[&'static str]) -> Result<Option<(&'static str, String)>> {
    optional_first_var_with(names, optional_var)
}

#[cfg(feature = "credentials-sts")]
pub(crate) fn optional_first_non_empty_var(
    names: &[&'static str],
) -> Result<Option<(&'static str, String)>> {
    optional_first_var_with(names, |name| {
        Ok(optional_var(name)?.filter(|value| !value.is_empty()))
    })
}

#[cfg(any(test, feature = "credentials-profile", feature = "credentials-sts"))]
fn optional_first_var_with<F>(
    names: &[&'static str],
    mut get: F,
) -> Result<Option<(&'static str, String)>>
where
    F: FnMut(&'static str) -> Result<Option<String>>,
{
    for name in names {
        if let Some(value) = get(name)? {
            return Ok(Some((name, value)));
        }
    }
    Ok(None)
}

#[cfg(any(feature = "credentials-imds", feature = "credentials-sts"))]
pub(crate) fn optional_non_empty_var(name: &'static str) -> Result<Option<String>> {
    Ok(optional_var(name)?.filter(|value| !value.is_empty()))
}

pub(crate) fn required_var(name: &'static str) -> Result<String> {
    optional_var(name)?.ok_or_else(|| Error::invalid_config(format!("missing {name}")))
}

#[cfg(feature = "credentials-sts")]
pub(crate) fn required_non_empty_var(name: &'static str) -> Result<String> {
    match optional_var(name)? {
        Some(value) if !value.is_empty() => Ok(value),
        Some(_) => Err(Error::invalid_config(format!("{name} must not be empty"))),
        None => Err(Error::invalid_config(format!("missing {name}"))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn optional_first_var_is_lazy() {
        let mut visited = Vec::new();
        let value = optional_first_var_with(&["PRIMARY", "FALLBACK"], |name| {
            visited.push(name);
            Ok((name == "PRIMARY").then(|| "value".to_string()))
        })
        .unwrap();

        assert_eq!(value, Some(("PRIMARY", "value".to_string())));
        assert_eq!(visited, ["PRIMARY"]);
    }

    #[test]
    fn optional_first_non_empty_var_skips_empty_values() {
        let mut visited = Vec::new();
        let value = optional_first_var_with(&["PRIMARY", "FALLBACK"], |name| {
            visited.push(name);
            Ok(match name {
                "PRIMARY" => Some(String::new()),
                "FALLBACK" => Some("fallback".to_string()),
                _ => None,
            }
            .filter(|value| !value.is_empty()))
        })
        .unwrap();

        assert_eq!(value, Some(("FALLBACK", "fallback".to_string())));
        assert_eq!(visited, ["PRIMARY", "FALLBACK"]);
    }

    #[test]
    fn optional_first_var_reports_first_error_before_fallback() {
        let err = optional_first_var_with(&["PRIMARY", "FALLBACK"], |name| {
            if name == "PRIMARY" {
                Err(Error::invalid_config("bad primary"))
            } else {
                Ok(Some("fallback".to_string()))
            }
        })
        .expect_err("primary error must stop fallback lookup");

        match err {
            Error::InvalidConfig { message } => assert!(message.contains("bad primary")),
            other => panic!("expected invalid config, got {other:?}"),
        }
    }
}