Skip to main content

omni_dev/utils/
env.rs

1//! Environment-variable dependency-injection seam.
2//!
3//! Production code reads the process environment only through an
4//! [`EnvSource`]. The real implementation, [`SystemEnv`], delegates to
5//! [`std::env::var`]; tests inject a pure in-memory fake
6//! (`crate::test_support::env::MapEnv`) instead of mutating the
7//! process-global environment.
8//!
9//! This removes the shared mutable global that the cross-module test race
10//! (issue #821) and the per-module env mutexes (#950, #1030) were fighting
11//! over: a test that constructs its own [`EnvSource`] never touches process
12//! env, so it needs no lock and runs fully in parallel. See
13//! [STYLE-0027](../../docs/STYLE_GUIDE.md) and `docs/plan/issue-1030-env-di.md`.
14//!
15//! `EnvSource` abstracts the **raw** environment only. The
16//! settings.json fallback layer composes on top of it — see
17//! [`crate::utils::settings`].
18
19/// A read-only view of environment variables.
20///
21/// Implemented by [`SystemEnv`] (the real process environment) in production
22/// and by an in-memory map in tests, so env-parsing boundaries can be tested
23/// without mutating the process-global environment.
24pub trait EnvSource {
25    /// Returns the value of `key`, or `None` if it is unset (or, for the
26    /// process environment, not valid Unicode).
27    fn var(&self, key: &str) -> Option<String>;
28
29    /// Returns the first set value among `keys`, in order.
30    fn var_any(&self, keys: &[&str]) -> Option<String> {
31        keys.iter().find_map(|k| self.var(k))
32    }
33}
34
35/// The real process environment, backed by [`std::env::var`].
36///
37/// This is the production [`EnvSource`]; pass `&SystemEnv` from the thin
38/// env-resolving wrapper that fronts each boundary seam.
39#[derive(Debug, Clone, Copy, Default)]
40pub struct SystemEnv;
41
42impl EnvSource for SystemEnv {
43    fn var(&self, key: &str) -> Option<String> {
44        std::env::var(key).ok()
45    }
46}
47
48/// `&T` is an `EnvSource` whenever `T` is, so callers can pass `&SystemEnv`
49/// or `&map_env` to functions taking `&impl EnvSource` without ceremony.
50impl<T: EnvSource + ?Sized> EnvSource for &T {
51    fn var(&self, key: &str) -> Option<String> {
52        (**self).var(key)
53    }
54}
55
56/// RAII guard that sets a process env var for the duration of a scope,
57/// restoring (or removing) whatever was there before when the guard drops —
58/// so a caller that mutates process env can be invoked more than once per
59/// process without leaking state between calls (#1538).
60pub(crate) struct ScopedEnvVar {
61    key: &'static str,
62    previous: Option<String>,
63}
64
65impl ScopedEnvVar {
66    /// Sets `key` to `value`, snapshotting the previous value (if any) to
67    /// restore on drop.
68    pub(crate) fn set(key: &'static str, value: &str) -> Self {
69        let previous = std::env::var(key).ok();
70        std::env::set_var(key, value);
71        Self { key, previous }
72    }
73}
74
75impl Drop for ScopedEnvVar {
76    fn drop(&mut self) {
77        match &self.previous {
78            Some(value) => std::env::set_var(self.key, value),
79            None => std::env::remove_var(self.key),
80        }
81    }
82}
83
84#[cfg(test)]
85#[allow(clippy::unwrap_used)]
86mod tests {
87    use super::*;
88    use crate::test_support::env::MapEnv;
89
90    #[test]
91    fn map_env_returns_inserted_values_and_none_otherwise() {
92        let env = MapEnv::new().with("USE_OPENAI", "true");
93        assert_eq!(env.var("USE_OPENAI").as_deref(), Some("true"));
94        assert_eq!(env.var("MISSING"), None);
95    }
96
97    #[test]
98    fn var_any_returns_first_set_key() {
99        let env = MapEnv::new().with("ANTHROPIC_API_KEY", "k");
100        assert_eq!(
101            env.var_any(&["CLAUDE_API_KEY", "ANTHROPIC_API_KEY"])
102                .as_deref(),
103            Some("k")
104        );
105        assert_eq!(env.var_any(&["A", "B"]), None);
106    }
107
108    #[test]
109    fn reference_forwards_to_inner_source() {
110        let env = MapEnv::new().with("K", "v");
111        fn read(src: &impl EnvSource) -> Option<String> {
112            src.var("K")
113        }
114        // &MapEnv must itself satisfy `impl EnvSource`.
115        assert_eq!(read(&&env).as_deref(), Some("v"));
116    }
117}