Skip to main content

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