Skip to main content

switchy_env/
lib.rs

1//! Switchy environment variable access with pluggable backends.
2//!
3//! This crate provides a unified interface for accessing environment variables with support
4//! for both standard system environment variables and a simulator mode for testing.
5//!
6//! # Features
7//!
8//! * **standard**: Uses `std::env` for real environment variable access
9//! * **simulator**: Provides a configurable environment for testing with deterministic defaults
10//!
11//! # Usage
12//!
13//! With the `standard` feature (default), access environment variables:
14//!
15//! ```rust
16//! # #[cfg(feature = "std")]
17//! # {
18//! use switchy_env::{var, var_parse};
19//!
20//! # unsafe { std::env::set_var("PORT", "8080"); }
21//! // Get a variable as a string
22//! let port_str = var("PORT").unwrap();
23//!
24//! // Parse a variable as a specific type
25//! let port: u16 = var_parse("PORT").unwrap();
26//! # }
27//! ```
28//!
29//! With the `simulator` feature, configure variables for testing:
30//!
31//! ```rust,ignore
32//! use switchy_env::{set_var, var, reset};
33//!
34//! // Set a test variable
35//! set_var("DATABASE_URL", "sqlite::memory:");
36//!
37//! // Access it like normal
38//! let db_url = var("DATABASE_URL").unwrap();
39//!
40//! // Reset to defaults
41//! reset();
42//! ```
43//!
44//! # Custom Providers
45//!
46//! Implement the [`EnvProvider`] trait to create custom environment variable sources:
47//!
48//! ```rust
49//! use switchy_env::{EnvProvider, EnvError, Result};
50//! use std::collections::BTreeMap;
51//!
52//! struct CustomEnv;
53//!
54//! impl EnvProvider for CustomEnv {
55//!     fn var(&self, name: &str) -> Result<String> {
56//!         // Custom logic here
57//!         Err(EnvError::NotFound(name.to_string()))
58//!     }
59//!
60//!     fn vars(&self) -> BTreeMap<String, String> {
61//!         BTreeMap::new()
62//!     }
63//! }
64//! ```
65
66#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
67#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
68#![allow(clippy::multiple_crate_versions)]
69
70use std::collections::BTreeMap;
71
72/// Standard system environment variable access.
73///
74/// This module provides access to real system environment variables via `std::env`.
75/// Enabled with the `std` feature (default).
76#[cfg(feature = "std")]
77pub mod standard;
78
79/// Simulator environment for testing.
80///
81/// This module provides a configurable environment with deterministic defaults
82/// for testing. Enabled with the `simulator` feature (default).
83#[cfg(feature = "simulator")]
84pub mod simulator;
85
86/// Error types for environment variable operations.
87///
88/// These errors can occur when accessing or parsing environment variables.
89#[derive(Debug, thiserror::Error)]
90pub enum EnvError {
91    /// Environment variable was not found
92    #[error("Environment variable '{0}' not found")]
93    NotFound(String),
94    /// Environment variable has an invalid value
95    #[error("Environment variable '{0}' has invalid value: {1}")]
96    InvalidValue(String, String),
97    /// Failed to parse environment variable value
98    #[error("Parse error for '{0}': {1}")]
99    ParseError(String, String),
100}
101
102/// Result type for environment variable operations.
103///
104/// A convenience type alias that uses [`EnvError`] as the error type.
105pub type Result<T> = std::result::Result<T, EnvError>;
106
107/// Trait for environment variable access.
108///
109/// This trait provides a unified interface for accessing environment variables
110/// from different sources (system environment, simulator, custom implementations).
111/// All providers must be thread-safe (`Send + Sync`).
112///
113/// # Examples
114///
115/// Implementing a custom provider:
116///
117/// ```rust
118/// use switchy_env::{EnvProvider, EnvError, Result};
119/// use std::collections::BTreeMap;
120///
121/// struct CustomEnv {
122///     vars: BTreeMap<String, String>,
123/// }
124///
125/// impl EnvProvider for CustomEnv {
126///     fn var(&self, name: &str) -> Result<String> {
127///         self.vars.get(name)
128///             .cloned()
129///             .ok_or_else(|| EnvError::NotFound(name.to_string()))
130///     }
131///
132///     fn vars(&self) -> BTreeMap<String, String> {
133///         self.vars.clone()
134///     }
135/// }
136/// ```
137pub trait EnvProvider: Send + Sync {
138    /// Get an environment variable as a string
139    ///
140    /// # Errors
141    ///
142    /// * If the environment variable is not found
143    fn var(&self, name: &str) -> Result<String>;
144
145    /// Get an environment variable with a default value
146    #[must_use]
147    fn var_or(&self, name: &str, default: &str) -> String {
148        self.var(name).unwrap_or_else(|_| default.to_string())
149    }
150
151    /// Get an environment variable parsed as a specific type
152    ///
153    /// # Errors
154    ///
155    /// * If the environment variable is not found
156    /// * If the environment variable value cannot be parsed to the target type
157    fn var_parse<T>(&self, name: &str) -> Result<T>
158    where
159        T: std::str::FromStr,
160        T::Err: std::fmt::Display,
161    {
162        let value = self.var(name)?;
163        value
164            .parse::<T>()
165            .map_err(|e| EnvError::ParseError(name.to_string(), e.to_string()))
166    }
167
168    /// Get an environment variable parsed with a default value
169    #[must_use]
170    fn var_parse_or<T>(&self, name: &str, default: T) -> T
171    where
172        T: std::str::FromStr,
173        T::Err: std::fmt::Display,
174    {
175        self.var_parse(name).unwrap_or(default)
176    }
177
178    /// Get an optional environment variable parsed as a specific type
179    ///
180    /// # Returns
181    ///
182    /// * `Ok(Some(value))` if the variable exists and parses successfully
183    /// * `Ok(None)` if the variable doesn't exist
184    /// * `Err(EnvError::ParseError)` if the variable exists but can't be parsed
185    ///
186    /// # Errors
187    ///
188    /// * If the environment variable fails to parse
189    fn var_parse_opt<T>(&self, name: &str) -> Result<Option<T>>
190    where
191        T: std::str::FromStr,
192        T::Err: std::fmt::Display,
193    {
194        match self.var(name) {
195            Ok(value) => value
196                .parse::<T>()
197                .map(Some)
198                .map_err(|e| EnvError::ParseError(name.to_string(), e.to_string())),
199            Err(EnvError::NotFound(_)) => Ok(None),
200            Err(e) => Err(e),
201        }
202    }
203
204    /// Check if an environment variable exists
205    #[must_use]
206    fn var_exists(&self, name: &str) -> bool {
207        self.var(name).is_ok()
208    }
209
210    /// Get all environment variables
211    #[must_use]
212    fn vars(&self) -> BTreeMap<String, String>;
213}
214
215#[allow(unused)]
216macro_rules! impl_env {
217    ($module:ident $(,)?) => {
218        pub use $module::{var, var_exists, var_or, var_parse, var_parse_opt, var_parse_or, vars};
219    };
220}
221
222#[cfg(feature = "simulator")]
223impl_env!(simulator);
224
225#[cfg(all(not(feature = "simulator"), feature = "std"))]
226impl_env!(standard);
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    struct TestEnvProvider {
233        vars: BTreeMap<String, String>,
234    }
235
236    impl EnvProvider for TestEnvProvider {
237        fn var(&self, name: &str) -> Result<String> {
238            self.vars
239                .get(name)
240                .cloned()
241                .ok_or_else(|| EnvError::NotFound(name.to_string()))
242        }
243
244        fn vars(&self) -> BTreeMap<String, String> {
245            self.vars.clone()
246        }
247    }
248
249    #[test_log::test]
250    fn test_custom_env_provider_implementation() {
251        let mut vars = BTreeMap::new();
252        vars.insert("KEY1".to_string(), "value1".to_string());
253        vars.insert("KEY2".to_string(), "value2".to_string());
254
255        let provider = TestEnvProvider { vars };
256
257        assert_eq!(provider.var("KEY1").unwrap(), "value1");
258        assert_eq!(provider.var("KEY2").unwrap(), "value2");
259        assert!(matches!(provider.var("KEY3"), Err(EnvError::NotFound(_))));
260    }
261
262    #[test_log::test]
263    fn test_env_provider_var_or() {
264        let provider = TestEnvProvider {
265            vars: BTreeMap::new(),
266        };
267
268        assert_eq!(provider.var_or("MISSING", "default"), "default");
269    }
270
271    #[test_log::test]
272    fn test_env_provider_var_or_with_existing() {
273        let mut vars = BTreeMap::new();
274        vars.insert("EXISTS".to_string(), "actual".to_string());
275
276        let provider = TestEnvProvider { vars };
277
278        assert_eq!(provider.var_or("EXISTS", "default"), "actual");
279    }
280
281    #[test_log::test]
282    fn test_env_provider_var_parse_success() {
283        let mut vars = BTreeMap::new();
284        vars.insert("NUMBER".to_string(), "42".to_string());
285
286        let provider = TestEnvProvider { vars };
287
288        let result: i32 = provider.var_parse("NUMBER").unwrap();
289        assert_eq!(result, 42);
290    }
291
292    #[test_log::test]
293    fn test_env_provider_var_parse_error() {
294        let mut vars = BTreeMap::new();
295        vars.insert("NOT_NUMBER".to_string(), "abc".to_string());
296
297        let provider = TestEnvProvider { vars };
298
299        let result: Result<i32> = provider.var_parse("NOT_NUMBER");
300        assert!(matches!(result, Err(EnvError::ParseError(_, _))));
301    }
302
303    #[test_log::test]
304    fn test_env_provider_var_parse_missing() {
305        let provider = TestEnvProvider {
306            vars: BTreeMap::new(),
307        };
308
309        let result: Result<i32> = provider.var_parse("MISSING");
310        assert!(matches!(result, Err(EnvError::NotFound(_))));
311    }
312
313    #[test_log::test]
314    fn test_env_provider_var_parse_or_success() {
315        let mut vars = BTreeMap::new();
316        vars.insert("NUMBER".to_string(), "100".to_string());
317
318        let provider = TestEnvProvider { vars };
319
320        let result: i32 = provider.var_parse_or("NUMBER", 42);
321        assert_eq!(result, 100);
322    }
323
324    #[test_log::test]
325    fn test_env_provider_var_parse_or_with_parse_error() {
326        let mut vars = BTreeMap::new();
327        vars.insert("NOT_NUMBER".to_string(), "xyz".to_string());
328
329        let provider = TestEnvProvider { vars };
330
331        let result: i32 = provider.var_parse_or("NOT_NUMBER", 42);
332        assert_eq!(result, 42);
333    }
334
335    #[test_log::test]
336    fn test_env_provider_var_parse_or_with_missing() {
337        let provider = TestEnvProvider {
338            vars: BTreeMap::new(),
339        };
340
341        let result: i32 = provider.var_parse_or("MISSING", 42);
342        assert_eq!(result, 42);
343    }
344
345    #[test_log::test]
346    fn test_env_provider_var_parse_opt_some() {
347        let mut vars = BTreeMap::new();
348        vars.insert("NUMBER".to_string(), "123".to_string());
349
350        let provider = TestEnvProvider { vars };
351
352        let result: Option<i32> = provider.var_parse_opt("NUMBER").unwrap();
353        assert_eq!(result, Some(123));
354    }
355
356    #[test_log::test]
357    fn test_env_provider_var_parse_opt_none() {
358        let provider = TestEnvProvider {
359            vars: BTreeMap::new(),
360        };
361
362        let result: Option<i32> = provider.var_parse_opt("MISSING").unwrap();
363        assert_eq!(result, None);
364    }
365
366    #[test_log::test]
367    fn test_env_provider_var_parse_opt_parse_error() {
368        let mut vars = BTreeMap::new();
369        vars.insert("NOT_NUMBER".to_string(), "not_an_int".to_string());
370
371        let provider = TestEnvProvider { vars };
372
373        let result: Result<Option<i32>> = provider.var_parse_opt("NOT_NUMBER");
374        assert!(matches!(result, Err(EnvError::ParseError(_, _))));
375    }
376
377    #[test_log::test]
378    fn test_env_provider_var_exists_true() {
379        let mut vars = BTreeMap::new();
380        vars.insert("EXISTS".to_string(), "yes".to_string());
381
382        let provider = TestEnvProvider { vars };
383
384        assert!(provider.var_exists("EXISTS"));
385    }
386
387    #[test_log::test]
388    fn test_env_provider_var_exists_false() {
389        let provider = TestEnvProvider {
390            vars: BTreeMap::new(),
391        };
392
393        assert!(!provider.var_exists("MISSING"));
394    }
395
396    #[test_log::test]
397    fn test_env_provider_vars() {
398        let mut vars = BTreeMap::new();
399        vars.insert("KEY1".to_string(), "value1".to_string());
400        vars.insert("KEY2".to_string(), "value2".to_string());
401
402        let provider = TestEnvProvider { vars: vars.clone() };
403
404        let result = provider.vars();
405        assert_eq!(result, vars);
406    }
407
408    #[test_log::test]
409    fn test_result_type_alias() {
410        let ok_result: Result<String> = Ok("test".to_string());
411        assert!(ok_result.is_ok());
412
413        let err_result: Result<String> = Err(EnvError::NotFound("VAR".to_string()));
414        assert!(err_result.is_err());
415    }
416
417    /// Provider that returns `InvalidValue` errors for specific variables
418    struct InvalidValueProvider {
419        invalid_vars: std::collections::BTreeSet<String>,
420    }
421
422    impl EnvProvider for InvalidValueProvider {
423        fn var(&self, name: &str) -> Result<String> {
424            if self.invalid_vars.contains(name) {
425                Err(EnvError::InvalidValue(
426                    name.to_string(),
427                    "contains null byte".to_string(),
428                ))
429            } else {
430                Err(EnvError::NotFound(name.to_string()))
431            }
432        }
433
434        fn vars(&self) -> BTreeMap<String, String> {
435            BTreeMap::new()
436        }
437    }
438
439    #[test_log::test]
440    fn test_env_provider_var_parse_opt_propagates_invalid_value_error() {
441        // This tests the `Err(e) => Err(e)` branch in var_parse_opt that propagates
442        // non-NotFound errors (like InvalidValue) from the underlying var() call.
443        let mut invalid_vars = std::collections::BTreeSet::new();
444        invalid_vars.insert("INVALID_VAR".to_string());
445
446        let provider = InvalidValueProvider { invalid_vars };
447
448        let result: Result<Option<i32>> = provider.var_parse_opt("INVALID_VAR");
449
450        // Should propagate the InvalidValue error, not return Ok(None)
451        assert!(matches!(
452            result,
453            Err(EnvError::InvalidValue(ref name, _)) if name == "INVALID_VAR"
454        ));
455    }
456
457    #[test_log::test]
458    fn test_env_provider_var_parse_propagates_invalid_value_error() {
459        // Test that var_parse also handles InvalidValue errors correctly
460        let mut invalid_vars = std::collections::BTreeSet::new();
461        invalid_vars.insert("INVALID_VAR".to_string());
462
463        let provider = InvalidValueProvider { invalid_vars };
464
465        let result: Result<i32> = provider.var_parse("INVALID_VAR");
466
467        // Should propagate the InvalidValue error
468        assert!(matches!(
469            result,
470            Err(EnvError::InvalidValue(ref name, _)) if name == "INVALID_VAR"
471        ));
472    }
473}