onetaskgraph_core/environment.rs
1//! The process environment, captured once.
2//!
3//! Everything downstream — the environment configuration layer, document discovery,
4//! and secret resolution — reads this snapshot rather than calling
5//! [`std::env::var`] itself. Two reasons, and only the second is about tests. A
6//! command that read the live environment twice could answer one question two ways
7//! if something changed between the reads. And a snapshot is a value, so the pure
8//! parsing and merging below it take one as an argument instead of reaching out of
9//! process.
10
11use std::collections::{BTreeMap, BTreeSet};
12use std::ffi::OsString;
13use std::fmt;
14
15/// A snapshot of the environment a process was started with.
16///
17/// # Debug redacts
18///
19/// This holds whatever the process was given, which on a configured host includes
20/// `LINEAR_API_KEY` and `GH_PROJECTS_TOKEN`. Deriving `Debug` would put both in
21/// any log line, panic message or `{:?}` that ever touched one, so the
22/// implementation below prints the *names* and never a value.
23#[derive(Clone, PartialEq, Eq, Default)]
24pub struct Environment {
25 variables: BTreeMap<String, String>,
26 unusable: BTreeSet<String>,
27}
28
29impl Environment {
30 /// Capture the environment this process was started with.
31 ///
32 /// Read as OS strings rather than through [`std::env::vars`], which panics on an
33 /// entry that is not valid Unicode — and a process is handed its environment by
34 /// whoever spawned it, so that is an input from outside rather than an invariant.
35 ///
36 /// A *name* that is not Unicode is dropped: no configuration document, no
37 /// `ONETASKGRAPH_` variable and no `api_key_env:` can spell one, so nothing in this
38 /// product could ever ask for it. A *value* that is not Unicode keeps its name in
39 /// [`unusable`](Self::unusable), where the environment layer turns a
40 /// `ONETASKGRAPH_`-prefixed one into a refusal naming the variable — because a
41 /// setting this product cannot read is exactly the thing it may not quietly ignore.
42 #[must_use]
43 pub fn from_process() -> Self {
44 Self::from_os_pairs(std::env::vars_os())
45 }
46
47 /// Build a snapshot from OS strings, sorting out what this product can read.
48 ///
49 /// Separate from [`from_process`](Self::from_process) so that the sorting is a
50 /// function of its argument: a hand-built pair is the only way to drive the two
51 /// not-valid-Unicode cases without one process spawning another.
52 #[must_use]
53 pub fn from_os_pairs(pairs: impl IntoIterator<Item = (OsString, OsString)>) -> Self {
54 let mut variables = BTreeMap::new();
55 let mut unusable = BTreeSet::new();
56 for (name, value) in pairs {
57 let Ok(name) = name.into_string() else {
58 continue;
59 };
60 match value.into_string() {
61 Ok(value) => {
62 variables.insert(name, value);
63 }
64 Err(_) => {
65 unusable.insert(name);
66 }
67 }
68 }
69 Self {
70 variables,
71 unusable,
72 }
73 }
74
75 /// Build a snapshot from explicit pairs.
76 #[must_use]
77 pub fn from_pairs<K, V>(pairs: impl IntoIterator<Item = (K, V)>) -> Self
78 where
79 K: Into<String>,
80 V: Into<String>,
81 {
82 Self {
83 variables: pairs
84 .into_iter()
85 .map(|(name, value)| (name.into(), value.into()))
86 .collect(),
87 unusable: BTreeSet::new(),
88 }
89 }
90
91 /// Every name this process was given whose value is not valid Unicode.
92 pub fn unusable(&self) -> impl Iterator<Item = &str> {
93 self.unusable.iter().map(String::as_str)
94 }
95
96 /// The value of `name`, or `None` when the snapshot does not define it.
97 #[must_use]
98 pub fn get(&self, name: &str) -> Option<&str> {
99 self.variables.get(name).map(String::as_str)
100 }
101
102 /// The value of `name` when it is set to something other than the empty string.
103 ///
104 /// An exported-but-empty variable is how a shell says "unset this for the child"
105 /// in practice, and treating `XDG_CONFIG_HOME=` as a path to the filesystem root
106 /// would send discovery somewhere nobody asked for.
107 #[must_use]
108 pub fn non_empty(&self, name: &str) -> Option<&str> {
109 self.get(name).filter(|value| !value.is_empty())
110 }
111
112 /// Every variable, name and value, in name order.
113 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
114 self.variables
115 .iter()
116 .map(|(name, value)| (name.as_str(), value.as_str()))
117 }
118
119 /// Every variable name, in order.
120 pub fn names(&self) -> impl Iterator<Item = &str> {
121 self.variables.keys().map(String::as_str)
122 }
123}
124
125impl fmt::Debug for Environment {
126 /// Names only. See the type's own note: a value here may be a live credential.
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.debug_struct("Environment")
129 .field("variables", &self.names().collect::<Vec<_>>())
130 .field("unusable", &self.unusable().collect::<Vec<_>>())
131 .field("values", &"<redacted>")
132 .finish()
133 }
134}