forge_core/env/
mod.rs

1//! Typesafe environment variable access for FORGE functions.
2//!
3//! This module provides a centralized, testable abstraction for reading environment
4//! variables. Instead of scattering `std::env::var()` calls throughout your functions,
5//! use `ctx.env()` methods which:
6//!
7//! - Provide type-safe parsing with clear error messages
8//! - Are easily mockable in tests via test context builders
9//! - Record which variables were accessed (for debugging)
10//!
11//! # Example
12//!
13//! ```ignore
14//! #[forge::action]
15//! async fn call_stripe(ctx: &ActionContext, input: ChargeInput) -> Result<Charge> {
16//!     // Get required env var (returns error if missing)
17//!     let api_key = ctx.env_require("STRIPE_API_KEY")?;
18//!
19//!     // Get optional env var with default
20//!     let timeout = ctx.env_or("STRIPE_TIMEOUT", "30");
21//!
22//!     // Get and parse to specific type
23//!     let max_retries: u32 = ctx.env_parse("STRIPE_MAX_RETRIES")?;
24//!
25//!     // ...
26//! }
27//! ```
28//!
29//! # Testing
30//!
31//! ```ignore
32//! #[test]
33//! fn test_stripe_action() {
34//!     let ctx = TestActionContext::builder()
35//!         .with_env("STRIPE_API_KEY", "sk_test_xxx")
36//!         .with_env("STRIPE_TIMEOUT", "60")
37//!         .build();
38//!
39//!     // Function will use mocked env vars
40//! }
41//! ```
42
43use std::collections::HashMap;
44use std::str::FromStr;
45use std::sync::{Arc, RwLock};
46
47use crate::{ForgeError, Result};
48
49/// Trait for environment variable access.
50///
51/// This abstraction allows production code to use real environment variables
52/// while tests can inject mock values.
53pub trait EnvProvider: Send + Sync {
54    /// Get an environment variable by name.
55    fn get(&self, key: &str) -> Option<String>;
56
57    /// Check if an environment variable is set.
58    fn contains(&self, key: &str) -> bool {
59        self.get(key).is_some()
60    }
61}
62
63/// Production environment provider that reads from `std::env`.
64#[derive(Debug, Clone, Default)]
65pub struct RealEnvProvider;
66
67impl RealEnvProvider {
68    /// Create a new real environment provider.
69    pub fn new() -> Self {
70        Self
71    }
72}
73
74impl EnvProvider for RealEnvProvider {
75    fn get(&self, key: &str) -> Option<String> {
76        std::env::var(key).ok()
77    }
78}
79
80/// Mock environment provider for testing.
81///
82/// Records which variables were accessed, useful for verifying that
83/// functions read the expected environment variables.
84#[derive(Debug, Clone, Default)]
85pub struct MockEnvProvider {
86    /// Configured environment variables.
87    vars: HashMap<String, String>,
88    /// Keys that were accessed (for verification).
89    accessed: Arc<RwLock<Vec<String>>>,
90}
91
92impl MockEnvProvider {
93    /// Create a new mock provider with no variables.
94    pub fn new() -> Self {
95        Self {
96            vars: HashMap::new(),
97            accessed: Arc::new(RwLock::new(Vec::new())),
98        }
99    }
100
101    /// Create a mock provider with the given variables.
102    pub fn with_vars(vars: HashMap<String, String>) -> Self {
103        Self {
104            vars,
105            accessed: Arc::new(RwLock::new(Vec::new())),
106        }
107    }
108
109    /// Set an environment variable.
110    pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
111        self.vars.insert(key.into(), value.into());
112    }
113
114    /// Remove an environment variable.
115    pub fn remove(&mut self, key: &str) {
116        self.vars.remove(key);
117    }
118
119    /// Get all configured variables.
120    pub fn all(&self) -> &HashMap<String, String> {
121        &self.vars
122    }
123
124    /// Get list of accessed variable names.
125    pub fn accessed_keys(&self) -> Vec<String> {
126        self.accessed.read().unwrap().clone()
127    }
128
129    /// Check if a specific key was accessed.
130    pub fn was_accessed(&self, key: &str) -> bool {
131        self.accessed.read().unwrap().contains(&key.to_string())
132    }
133
134    /// Clear the accessed keys list.
135    pub fn clear_accessed(&self) {
136        self.accessed.write().unwrap().clear();
137    }
138
139    /// Assert that a specific key was accessed.
140    pub fn assert_accessed(&self, key: &str) {
141        assert!(
142            self.was_accessed(key),
143            "Expected env var '{}' to be accessed, but it wasn't. Accessed keys: {:?}",
144            key,
145            self.accessed_keys()
146        );
147    }
148
149    /// Assert that a specific key was NOT accessed.
150    pub fn assert_not_accessed(&self, key: &str) {
151        assert!(
152            !self.was_accessed(key),
153            "Expected env var '{}' to NOT be accessed, but it was",
154            key
155        );
156    }
157}
158
159impl EnvProvider for MockEnvProvider {
160    fn get(&self, key: &str) -> Option<String> {
161        // Record access
162        self.accessed.write().unwrap().push(key.to_string());
163        self.vars.get(key).cloned()
164    }
165}
166
167/// Extension methods for environment variable access on contexts.
168///
169/// This is implemented as a separate trait to avoid code duplication
170/// across different context types.
171pub trait EnvAccess {
172    /// Get the environment provider.
173    fn env_provider(&self) -> &dyn EnvProvider;
174
175    /// Get an environment variable.
176    ///
177    /// Returns `None` if the variable is not set.
178    fn env(&self, key: &str) -> Option<String> {
179        self.env_provider().get(key)
180    }
181
182    /// Get an environment variable with a default value.
183    ///
184    /// Returns the default if the variable is not set.
185    fn env_or(&self, key: &str, default: &str) -> String {
186        self.env_provider()
187            .get(key)
188            .unwrap_or_else(|| default.to_string())
189    }
190
191    /// Get a required environment variable.
192    ///
193    /// Returns an error if the variable is not set.
194    fn env_require(&self, key: &str) -> Result<String> {
195        self.env_provider().get(key).ok_or_else(|| {
196            ForgeError::Config(format!("Required environment variable '{}' not set", key))
197        })
198    }
199
200    /// Get an environment variable and parse it to the specified type.
201    ///
202    /// Returns an error if:
203    /// - The variable is not set
204    /// - The value cannot be parsed to the target type
205    fn env_parse<T: FromStr>(&self, key: &str) -> Result<T>
206    where
207        T::Err: std::fmt::Display,
208    {
209        let value = self.env_require(key)?;
210        value.parse().map_err(|e: T::Err| {
211            ForgeError::Config(format!(
212                "Failed to parse env var '{}' value '{}': {}",
213                key, value, e
214            ))
215        })
216    }
217
218    /// Get an environment variable and parse it, with a default.
219    ///
220    /// Returns the default if the variable is not set.
221    /// Returns an error only if the variable IS set but cannot be parsed.
222    fn env_parse_or<T: FromStr>(&self, key: &str, default: T) -> Result<T>
223    where
224        T::Err: std::fmt::Display,
225    {
226        match self.env_provider().get(key) {
227            Some(value) => value.parse().map_err(|e: T::Err| {
228                ForgeError::Config(format!(
229                    "Failed to parse env var '{}' value '{}': {}",
230                    key, value, e
231                ))
232            }),
233            None => Ok(default),
234        }
235    }
236
237    /// Check if an environment variable is set.
238    fn env_contains(&self, key: &str) -> bool {
239        self.env_provider().contains(key)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_real_env_provider() {
249        // Set a test env var
250        unsafe {
251            std::env::set_var("FORGE_TEST_VAR", "test_value");
252        }
253
254        let provider = RealEnvProvider::new();
255        assert_eq!(
256            provider.get("FORGE_TEST_VAR"),
257            Some("test_value".to_string())
258        );
259        assert!(provider.contains("FORGE_TEST_VAR"));
260        assert!(provider.get("FORGE_NONEXISTENT_VAR").is_none());
261
262        // Cleanup
263        unsafe {
264            std::env::remove_var("FORGE_TEST_VAR");
265        }
266    }
267
268    #[test]
269    fn test_mock_env_provider() {
270        let mut provider = MockEnvProvider::new();
271        provider.set("API_KEY", "secret123");
272        provider.set("TIMEOUT", "30");
273
274        assert_eq!(provider.get("API_KEY"), Some("secret123".to_string()));
275        assert_eq!(provider.get("TIMEOUT"), Some("30".to_string()));
276        assert!(provider.get("MISSING").is_none());
277
278        // Check access tracking
279        assert!(provider.was_accessed("API_KEY"));
280        assert!(provider.was_accessed("TIMEOUT"));
281        assert!(provider.was_accessed("MISSING")); // Even failed lookups are tracked
282
283        provider.assert_accessed("API_KEY");
284    }
285
286    #[test]
287    fn test_mock_provider_with_vars() {
288        let vars = HashMap::from([
289            ("KEY1".to_string(), "value1".to_string()),
290            ("KEY2".to_string(), "value2".to_string()),
291        ]);
292        let provider = MockEnvProvider::with_vars(vars);
293
294        assert_eq!(provider.get("KEY1"), Some("value1".to_string()));
295        assert_eq!(provider.get("KEY2"), Some("value2".to_string()));
296    }
297
298    #[test]
299    fn test_clear_accessed() {
300        let mut provider = MockEnvProvider::new();
301        provider.set("KEY", "value");
302
303        provider.get("KEY");
304        assert!(!provider.accessed_keys().is_empty());
305
306        provider.clear_accessed();
307        assert!(provider.accessed_keys().is_empty());
308    }
309
310    // Test EnvAccess trait methods using a simple wrapper
311    struct TestEnvContext {
312        provider: MockEnvProvider,
313    }
314
315    impl EnvAccess for TestEnvContext {
316        fn env_provider(&self) -> &dyn EnvProvider {
317            &self.provider
318        }
319    }
320
321    #[test]
322    fn test_env_access_methods() {
323        let mut provider = MockEnvProvider::new();
324        provider.set("PORT", "8080");
325        provider.set("DEBUG", "true");
326        provider.set("BAD_NUMBER", "not_a_number");
327
328        let ctx = TestEnvContext { provider };
329
330        // env()
331        assert_eq!(ctx.env("PORT"), Some("8080".to_string()));
332        assert!(ctx.env("MISSING").is_none());
333
334        // env_or()
335        assert_eq!(ctx.env_or("PORT", "3000"), "8080");
336        assert_eq!(ctx.env_or("MISSING", "default"), "default");
337
338        // env_require()
339        assert_eq!(ctx.env_require("PORT").unwrap(), "8080");
340        assert!(ctx.env_require("MISSING").is_err());
341
342        // env_parse()
343        let port: u16 = ctx.env_parse("PORT").unwrap();
344        assert_eq!(port, 8080);
345
346        let debug: bool = ctx.env_parse("DEBUG").unwrap();
347        assert!(debug);
348
349        // Parse error
350        let bad: Result<u32> = ctx.env_parse("BAD_NUMBER");
351        assert!(bad.is_err());
352
353        // env_parse_or()
354        let port: u16 = ctx.env_parse_or("MISSING", 3000).unwrap();
355        assert_eq!(port, 3000);
356
357        // env_contains()
358        assert!(ctx.env_contains("PORT"));
359        assert!(!ctx.env_contains("MISSING"));
360    }
361}