Skip to main content

standout_input/sources/
env.rs

1//! Environment variable input source.
2
3use std::sync::Arc;
4
5use clap::ArgMatches;
6
7use crate::collector::InputCollector;
8use crate::env::{EnvReader, RealEnv};
9use crate::InputError;
10
11/// Collect input from an environment variable.
12///
13/// This source reads from an environment variable. It is available when
14/// the variable is set and non-empty.
15///
16/// # Example
17///
18/// ```ignore
19/// use standout_input::{InputChain, ArgSource, EnvSource};
20///
21/// // For: MY_TOKEN=secret myapp
22/// let chain = InputChain::<String>::new()
23///     .try_source(ArgSource::new("token"))
24///     .try_source(EnvSource::new("MY_TOKEN"));
25/// ```
26///
27/// # Testing
28///
29/// Use [`EnvSource::with_reader`] to inject a mock for testing:
30///
31/// ```ignore
32/// use standout_input::{EnvSource, MockEnv};
33///
34/// let env = MockEnv::new().with_var("MY_TOKEN", "secret");
35/// let source = EnvSource::with_reader("MY_TOKEN", env);
36/// ```
37#[derive(Clone)]
38pub struct EnvSource<R: EnvReader = RealEnv> {
39    var_name: String,
40    reader: Arc<R>,
41}
42
43impl EnvSource<RealEnv> {
44    /// Create a new environment variable source.
45    pub fn new(var_name: impl Into<String>) -> Self {
46        Self {
47            var_name: var_name.into(),
48            reader: Arc::new(RealEnv),
49        }
50    }
51}
52
53impl<R: EnvReader> EnvSource<R> {
54    /// Create an environment source with a custom reader.
55    ///
56    /// This is primarily used for testing to inject mock environment.
57    pub fn with_reader(var_name: impl Into<String>, reader: R) -> Self {
58        Self {
59            var_name: var_name.into(),
60            reader: Arc::new(reader),
61        }
62    }
63
64    /// Get the environment variable name.
65    pub fn var_name(&self) -> &str {
66        &self.var_name
67    }
68}
69
70impl<R: EnvReader + 'static> InputCollector<String> for EnvSource<R> {
71    fn name(&self) -> &'static str {
72        "environment variable"
73    }
74
75    fn is_available(&self, _matches: &ArgMatches) -> bool {
76        self.reader
77            .var(&self.var_name)
78            .map(|v| !v.is_empty())
79            .unwrap_or(false)
80    }
81
82    fn collect(&self, _matches: &ArgMatches) -> Result<Option<String>, InputError> {
83        match self.reader.var(&self.var_name) {
84            Some(value) if !value.is_empty() => Ok(Some(value)),
85            _ => Ok(None),
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::env::MockEnv;
94    use clap::Command;
95
96    fn empty_matches() -> ArgMatches {
97        Command::new("test").try_get_matches_from(["test"]).unwrap()
98    }
99
100    #[test]
101    fn env_available_when_set() {
102        let env = MockEnv::new().with_var("MY_VAR", "value");
103        let source = EnvSource::with_reader("MY_VAR", env);
104
105        assert!(source.is_available(&empty_matches()));
106    }
107
108    #[test]
109    fn env_unavailable_when_unset() {
110        let env = MockEnv::new();
111        let source = EnvSource::with_reader("MY_VAR", env);
112
113        assert!(!source.is_available(&empty_matches()));
114    }
115
116    #[test]
117    fn env_unavailable_when_empty() {
118        let env = MockEnv::new().with_var("MY_VAR", "");
119        let source = EnvSource::with_reader("MY_VAR", env);
120
121        assert!(!source.is_available(&empty_matches()));
122    }
123
124    #[test]
125    fn env_collects_value() {
126        let env = MockEnv::new().with_var("MY_VAR", "hello");
127        let source = EnvSource::with_reader("MY_VAR", env);
128
129        let result = source.collect(&empty_matches()).unwrap();
130        assert_eq!(result, Some("hello".to_string()));
131    }
132
133    #[test]
134    fn env_returns_none_when_unset() {
135        let env = MockEnv::new();
136        let source = EnvSource::with_reader("MY_VAR", env);
137
138        let result = source.collect(&empty_matches()).unwrap();
139        assert_eq!(result, None);
140    }
141
142    #[test]
143    fn env_returns_none_when_empty() {
144        let env = MockEnv::new().with_var("MY_VAR", "");
145        let source = EnvSource::with_reader("MY_VAR", env);
146
147        let result = source.collect(&empty_matches()).unwrap();
148        assert_eq!(result, None);
149    }
150}