datasynth_config/
env_interpolation.rs1use regex::Regex;
8use std::env;
9use thiserror::Error;
10
11#[derive(Debug, Error)]
12pub enum EnvInterpolationError {
13 #[error("Environment variable '{0}' is not set and no default provided")]
14 MissingVariable(String),
15}
16
17pub fn interpolate_env(input: &str) -> Result<String, EnvInterpolationError> {
37 let re = Regex::new(r"\$\{([^}]+)\}").expect("valid env interpolation regex");
38 let mut result = input.to_string();
39 let mut errors = Vec::new();
40
41 let matches: Vec<(String, String)> = re
43 .captures_iter(input)
44 .map(|cap| {
45 let full_match = cap
46 .get(0)
47 .expect("capture group 0 always exists")
48 .as_str()
49 .to_string();
50 let inner = cap
51 .get(1)
52 .expect("capture group 1 defined in regex")
53 .as_str()
54 .to_string();
55 (full_match, inner)
56 })
57 .collect();
58
59 for (full_match, inner) in matches {
60 let replacement = if let Some((var_name, default_value)) = inner.split_once(":-") {
61 match env::var(var_name) {
63 Ok(val) => val,
64 Err(_) => default_value.to_string(),
65 }
66 } else {
67 match env::var(&inner) {
69 Ok(val) => val,
70 Err(_) => {
71 errors.push(inner.clone());
72 continue;
73 }
74 }
75 };
76
77 result = result.replace(&full_match, &replacement);
78 }
79
80 if let Some(first_error) = errors.into_iter().next() {
81 return Err(EnvInterpolationError::MissingVariable(first_error));
82 }
83
84 Ok(result)
85}
86
87#[cfg(test)]
88#[allow(clippy::unwrap_used)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_basic_substitution() {
94 env::set_var("TEST_INTERP_VAR", "hello");
95 let result = interpolate_env("value: ${TEST_INTERP_VAR}").unwrap();
96 assert_eq!(result, "value: hello");
97 env::remove_var("TEST_INTERP_VAR");
98 }
99
100 #[test]
101 fn test_default_value() {
102 env::remove_var("TEST_INTERP_MISSING");
103 let result = interpolate_env("value: ${TEST_INTERP_MISSING:-fallback}").unwrap();
104 assert_eq!(result, "value: fallback");
105 }
106
107 #[test]
108 fn test_default_with_existing_var() {
109 env::set_var("TEST_INTERP_EXISTS", "real_value");
110 let result = interpolate_env("value: ${TEST_INTERP_EXISTS:-fallback}").unwrap();
111 assert_eq!(result, "value: real_value");
112 env::remove_var("TEST_INTERP_EXISTS");
113 }
114
115 #[test]
116 fn test_missing_required_variable() {
117 env::remove_var("TEST_INTERP_REQUIRED");
118 let result = interpolate_env("value: ${TEST_INTERP_REQUIRED}");
119 assert!(result.is_err());
120 }
121
122 #[test]
123 fn test_no_interpolation_needed() {
124 let result = interpolate_env("plain text without variables").unwrap();
125 assert_eq!(result, "plain text without variables");
126 }
127
128 #[test]
129 fn test_multiple_variables() {
130 env::set_var("TEST_INTERP_A", "alpha");
131 env::set_var("TEST_INTERP_B", "beta");
132 let result = interpolate_env("${TEST_INTERP_A} and ${TEST_INTERP_B}").unwrap();
133 assert_eq!(result, "alpha and beta");
134 env::remove_var("TEST_INTERP_A");
135 env::remove_var("TEST_INTERP_B");
136 }
137
138 #[test]
139 fn test_empty_default() {
140 env::remove_var("TEST_INTERP_EMPTY_DEFAULT");
141 let result = interpolate_env("value: ${TEST_INTERP_EMPTY_DEFAULT:-}").unwrap();
142 assert_eq!(result, "value: ");
143 }
144}