Skip to main content

oino_models/
env.rs

1//! Environment lookup used by credential and config-value resolution.
2//!
3//! The environment is injected so tests stay deterministic
4//! (edition 2024 makes `std::env::set_var` unsafe) and hosts can overlay variables.
5
6use std::collections::HashMap;
7use std::fmt;
8
9/// Source of environment variables.
10///
11/// Implementations return `None` for unset variables. Callers treat empty values as unset,
12/// matching `env[name] || process.env[name] || undefined`.
13pub trait EnvSource: Send + Sync {
14    /// Value of `name`, if set.
15    fn var(&self, name: &str) -> Option<String>;
16}
17
18/// The real process environment (`std::env::var`).
19#[derive(Clone, Copy, Debug, Default)]
20pub struct ProcessEnv;
21
22impl EnvSource for ProcessEnv {
23    fn var(&self, name: &str) -> Option<String> {
24        std::env::var(name).ok()
25    }
26}
27
28/// A fixed environment map, for tests and sandboxed hosts.
29#[derive(Clone, Default, PartialEq, Eq)]
30pub struct MapEnv(HashMap<String, String>);
31
32impl MapEnv {
33    /// Empty environment.
34    #[must_use]
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Adds or replaces `name`.
40    #[must_use]
41    pub fn with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
42        self.0.insert(name.into(), value.into());
43        self
44    }
45}
46
47impl fmt::Debug for MapEnv {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        // Values may be secrets: only print names.
50        let mut names: Vec<&String> = self.0.keys().collect();
51        names.sort();
52        f.debug_tuple("MapEnv").field(&names).finish()
53    }
54}
55
56impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for MapEnv {
57    fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
58        Self(
59            iter.into_iter()
60                .map(|(k, v)| (k.into(), v.into()))
61                .collect(),
62        )
63    }
64}
65
66impl EnvSource for MapEnv {
67    fn var(&self, name: &str) -> Option<String> {
68        self.0.get(name).cloned()
69    }
70}
71
72/// Looks up `name` in `explicit` first, then in `env`; empty values count as unset
73///.
74pub(crate) fn lookup(
75    name: &str,
76    explicit: Option<&indexmap::IndexMap<String, String>>,
77    env: &dyn EnvSource,
78) -> Option<String> {
79    explicit
80        .and_then(|map| map.get(name))
81        .filter(|value| !value.is_empty())
82        .cloned()
83        .or_else(|| env.var(name).filter(|value| !value.is_empty()))
84}