1use std::{env, fmt, mem, sync::Arc};
2
3use anyhow::Context as _;
4
5use super::{ConfigSource, Flat};
6use crate::{
7 Json,
8 testing::MOCK_ENV_VARS,
9 utils::JsonObject,
10 value::{FileFormat, Map, Value, ValueOrigin, WithOrigin},
11};
12
13#[derive(Debug, Clone)]
15pub struct Environment {
16 origin: Arc<ValueOrigin>,
17 map: Map,
18}
19
20impl Default for Environment {
21 fn default() -> Self {
22 Self {
23 origin: Arc::new(ValueOrigin::EnvVars),
24 map: Map::new(),
25 }
26 }
27}
28
29impl Environment {
30 pub fn prefixed(prefix: &str) -> Self {
32 MOCK_ENV_VARS.with_borrow(|mock_vars| {
33 let mock_vars = mock_vars
34 .iter()
35 .map(|(key, value)| (key.clone(), value.clone()));
36 Self::from_iter(prefix, env::vars().chain(mock_vars))
37 })
38 }
39
40 pub fn from_iter<K, V>(prefix: &str, env: impl IntoIterator<Item = (K, V)>) -> Self
42 where
43 K: AsRef<str> + Into<String>,
44 V: Into<String>,
45 {
46 let origin = Arc::new(ValueOrigin::EnvVars);
47 let map = env.into_iter().filter_map(|(name, value)| {
48 let retained_name = name.as_ref().strip_prefix(prefix)?.to_lowercase();
49 Some((
50 retained_name,
51 WithOrigin {
52 inner: Value::from(value.into()),
53 origin: Arc::new(ValueOrigin::Path {
54 source: origin.clone(),
55 path: name.into(),
56 }),
57 },
58 ))
59 });
60 let map = map.collect();
61 Self { origin, map }
62 }
63
64 #[must_use]
66 pub fn with_vars(mut self, var_names: &[&str]) -> Self {
67 let origin = Arc::new(ValueOrigin::EnvVars);
68 let defined_vars = var_names.iter().filter_map(|&name| {
69 let value = env::var_os(name)?.into_string().ok()?;
70 Some((
71 name.to_owned(),
72 WithOrigin {
73 inner: Value::from(value),
74 origin: Arc::new(ValueOrigin::Path {
75 source: origin.clone(),
76 path: name.to_owned(),
77 }),
78 },
79 ))
80 });
81 self.map.extend(defined_vars);
82 self
83 }
84
85 #[doc(hidden)] pub fn from_dotenv(filename: &str, contents: &str) -> anyhow::Result<Self> {
87 let origin = Arc::new(ValueOrigin::File {
88 name: filename.to_owned(),
89 format: FileFormat::Dotenv,
90 });
91 let mut map = Map::default();
92 for line in contents.lines().map(str::trim) {
93 if line.is_empty() || line.starts_with('#') {
94 continue;
95 }
96 let (name, variable_value) = line.split_once('=').with_context(|| {
97 format!("Incorrect line for setting environment variable: {line}")
98 })?;
99 let variable_value = variable_value.trim_matches('"');
100 map.insert(
101 name.to_lowercase(),
102 WithOrigin {
103 inner: Value::from(variable_value.to_owned()),
104 origin: Arc::new(ValueOrigin::Path {
105 source: origin.clone(),
106 path: name.into(),
107 }),
108 },
109 );
110 }
111 Ok(Self { origin, map })
112 }
113
114 pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &WithOrigin)> + '_ {
116 self.map.iter().map(|(name, value)| (name.as_str(), value))
117 }
118
119 #[must_use]
121 pub fn strip_prefix(self, prefix: &str) -> Self {
122 let prefix = prefix.to_lowercase();
123 let filtered = self
124 .map
125 .into_iter()
126 .filter_map(|(name, value)| Some((name.strip_prefix(&prefix)?.to_owned(), value)));
127 Self {
128 origin: self.origin,
129 map: filtered.collect(),
130 }
131 }
132
133 pub fn coerce_json(&mut self) -> anyhow::Result<()> {
139 let mut coerced_values = vec![];
140 let mut errors = vec![];
141 for (key, value) in &self.map {
142 let stripped_key = key
143 .strip_suffix("__json")
144 .or_else(|| key.strip_suffix(":json"));
145 let Some(stripped_key) = stripped_key else {
146 continue;
147 };
148 let Some(value_str) = value.inner.as_plain_str() else {
149 continue;
151 };
152
153 let val = match serde_json::from_str::<serde_json::Value>(value_str) {
154 Ok(val) => val,
155 Err(err) => {
156 mem::take(&mut coerced_values);
157 errors.push((value.origin.clone(), err));
158 continue;
159 }
160 };
161 if !errors.is_empty() {
162 continue; }
164
165 let root_origin = Arc::new(ValueOrigin::Synthetic {
166 source: value.origin.clone(),
167 transform: "parsed JSON string".into(),
168 });
169 let coerced_value = Json::map_value(val, &root_origin, String::new());
170 coerced_values.push((key.to_owned(), stripped_key.to_owned(), coerced_value));
171 }
172
173 for (key, stripped_key, coerced_value) in coerced_values {
174 self.map.remove(&key);
175 self.map.insert(stripped_key, coerced_value);
176 }
177
178 if errors.is_empty() {
179 Ok(())
180 } else {
181 Err(JsonCoercionErrors(errors).into())
182 }
183 }
184
185 pub fn convert_flat_params(flat_params: &JsonObject, prefix: &str) -> JsonObject {
195 let vars = flat_params.iter().map(|(path, value)| {
196 let mut var_name = path.replace('.', "_").to_uppercase();
197 var_name.insert_str(0, prefix);
198 let value: serde_json::Value = match value {
199 serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
200 var_name.push_str("__JSON");
201 value.to_string().into()
202 }
203 simple => simple.clone(),
204 };
205 (var_name, value)
206 });
207 vars.collect()
208 }
209}
210
211#[derive(Debug)]
212struct JsonCoercionErrors(Vec<(Arc<ValueOrigin>, serde_json::Error)>);
213
214impl fmt::Display for JsonCoercionErrors {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 writeln!(
217 formatter,
218 "failed coercing flat configuration params to JSON:"
219 )?;
220 for (i, (key, err)) in self.0.iter().enumerate() {
221 writeln!(formatter, "{}. {key}: {err}", i + 1)?;
222 }
223 Ok(())
224 }
225}
226
227impl std::error::Error for JsonCoercionErrors {}
228
229impl ConfigSource for Environment {
230 type Kind = Flat;
231
232 fn into_contents(self) -> WithOrigin<Map> {
233 WithOrigin::new(self.map, self.origin)
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use assert_matches::assert_matches;
240
241 use super::*;
242
243 #[test]
244 fn parsing_dotenv_contents() {
245 let env = Environment::from_dotenv(
246 "test.env",
247 r#"
248 APP_TEST=what
249 APP_OTHER="test string"
250
251 # Overwriting vars should be supported
252 APP_TEST=42
253 "#,
254 )
255 .unwrap();
256
257 assert_eq!(env.map.len(), 2, "{:?}", env.map);
258 assert_eq!(env.map["app_test"].inner.as_plain_str(), Some("42"));
259 let origin = &env.map["app_test"].origin;
260 let ValueOrigin::Path { path, source } = origin.as_ref() else {
261 panic!("unexpected origin: {origin:?}");
262 };
263 assert_eq!(path, "APP_TEST");
264 assert_matches!(
265 source.as_ref(),
266 ValueOrigin::File { name, format: FileFormat::Dotenv } if name == "test.env"
267 );
268 assert_eq!(
269 env.map["app_other"].inner.as_plain_str(),
270 Some("test string")
271 );
272
273 let env = env.strip_prefix("app_");
274 assert_eq!(env.map.len(), 2, "{:?}", env.map);
275 assert_eq!(env.map["test"].inner.as_plain_str(), Some("42"));
276 assert_matches!(env.map["test"].origin.as_ref(), ValueOrigin::Path { path, .. } if path == "APP_TEST");
277 assert_eq!(env.map["other"].inner.as_plain_str(), Some("test string"));
278 }
279
280 #[test]
281 fn converting_flat_params() {
282 let params = serde_json::json!({
283 "value": 23,
284 "flag": true,
285 "nested.option": null,
286 "nested.renamed": "first",
287 "nested.set": ["first", "second"],
288 "nested.map": { "call": 42 },
289 });
290 let params = params.as_object().unwrap();
291
292 let converted = Environment::convert_flat_params(params, "APP_");
293 assert_eq!(
294 serde_json::Value::from(converted),
295 serde_json::json!({
296 "APP_VALUE": 23,
297 "APP_FLAG": true,
298 "APP_NESTED_OPTION": null,
299 "APP_NESTED_RENAMED": "first",
300 "APP_NESTED_SET__JSON": r#"["first","second"]"#,
301 "APP_NESTED_MAP__JSON": r#"{"call":42}"#,
302 })
303 );
304 }
305}