standout-input 7.2.0

Declarative input collection for CLI applications
Documentation
//! Environment variable input source.

use std::sync::Arc;

use clap::ArgMatches;

use crate::collector::InputCollector;
use crate::env::{EnvReader, RealEnv};
use crate::InputError;

/// Collect input from an environment variable.
///
/// This source reads from an environment variable. It is available when
/// the variable is set and non-empty.
///
/// # Example
///
/// ```ignore
/// use standout_input::{InputChain, ArgSource, EnvSource};
///
/// // For: MY_TOKEN=secret myapp
/// let chain = InputChain::<String>::new()
///     .try_source(ArgSource::new("token"))
///     .try_source(EnvSource::new("MY_TOKEN"));
/// ```
///
/// # Testing
///
/// Use [`EnvSource::with_reader`] to inject a mock for testing:
///
/// ```ignore
/// use standout_input::{EnvSource, MockEnv};
///
/// let env = MockEnv::new().with_var("MY_TOKEN", "secret");
/// let source = EnvSource::with_reader("MY_TOKEN", env);
/// ```
#[derive(Clone)]
pub struct EnvSource<R: EnvReader = RealEnv> {
    var_name: String,
    reader: Arc<R>,
}

impl EnvSource<RealEnv> {
    /// Create a new environment variable source.
    pub fn new(var_name: impl Into<String>) -> Self {
        Self {
            var_name: var_name.into(),
            reader: Arc::new(RealEnv),
        }
    }
}

impl<R: EnvReader> EnvSource<R> {
    /// Create an environment source with a custom reader.
    ///
    /// This is primarily used for testing to inject mock environment.
    pub fn with_reader(var_name: impl Into<String>, reader: R) -> Self {
        Self {
            var_name: var_name.into(),
            reader: Arc::new(reader),
        }
    }

    /// Get the environment variable name.
    pub fn var_name(&self) -> &str {
        &self.var_name
    }
}

impl<R: EnvReader + 'static> InputCollector<String> for EnvSource<R> {
    fn name(&self) -> &'static str {
        "environment variable"
    }

    fn is_available(&self, _matches: &ArgMatches) -> bool {
        self.reader
            .var(&self.var_name)
            .map(|v| !v.is_empty())
            .unwrap_or(false)
    }

    fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
        match self.reader.var(&self.var_name) {
            Some(value) if !value.is_empty() => Ok(Some(value)),
            _ => Ok(None),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::env::MockEnv;
    use clap::Command;

    fn empty_matches() -> ArgMatches {
        Command::new("test").try_get_matches_from(["test"]).unwrap()
    }

    #[test]
    fn env_available_when_set() {
        let env = MockEnv::new().with_var("MY_VAR", "value");
        let source = EnvSource::with_reader("MY_VAR", env);

        assert!(source.is_available(&empty_matches()));
    }

    #[test]
    fn env_unavailable_when_unset() {
        let env = MockEnv::new();
        let source = EnvSource::with_reader("MY_VAR", env);

        assert!(!source.is_available(&empty_matches()));
    }

    #[test]
    fn env_unavailable_when_empty() {
        let env = MockEnv::new().with_var("MY_VAR", "");
        let source = EnvSource::with_reader("MY_VAR", env);

        assert!(!source.is_available(&empty_matches()));
    }

    #[test]
    fn env_collects_value() {
        let env = MockEnv::new().with_var("MY_VAR", "hello");
        let source = EnvSource::with_reader("MY_VAR", env);

        let result = source.collect(&empty_matches()).unwrap();
        assert_eq!(result, Some("hello".to_string()));
    }

    #[test]
    fn env_returns_none_when_unset() {
        let env = MockEnv::new();
        let source = EnvSource::with_reader("MY_VAR", env);

        let result = source.collect(&empty_matches()).unwrap();
        assert_eq!(result, None);
    }

    #[test]
    fn env_returns_none_when_empty() {
        let env = MockEnv::new().with_var("MY_VAR", "");
        let source = EnvSource::with_reader("MY_VAR", env);

        let result = source.collect(&empty_matches()).unwrap();
        assert_eq!(result, None);
    }
}