Maintain/Build/Rhai/EnvironmentResolver.rs
1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/Rhai/EnvironmentResolver.rs
3//=============================================================================//
4// Module: EnvironmentResolver
5//
6// Brief Description: Resolves final environment variables from all sources.
7//
8// RESPONSIBILITIES:
9// ================
10//
11// Primary:
12// - Merge environment variables from multiple sources
13// - Add environment variable prefixes per crate
14// - Apply environment variables to current process
15// - Provide validation of required environment variables
16// - Support workbench and feature flag resolution
17//
18// Secondary:
19// - Log environment variable resolution
20// - Support variable expansion (e.g., ${VAR})
21// - Generate feature flag environment variables
22//
23//=============================================================================//
24
25use std::collections::HashMap;
26
27//=============================================================================
28// Public API
29//=============================================================================
30
31/// Resolves the final set of environment variables.
32///
33/// This function merges variables from:
34/// 1. Template defaults
35/// 2. Profile-specific static variables
36/// 3. Workbench environment variables
37/// 4. Feature flag variables
38/// 5. Rhai script output
39/// 6. Current process environment
40///
41/// # Arguments
42///
43/// * `template_env` - Environment variables from templates
44/// * `profile_env` - Environment variables from profile
45/// * `workbench_env` - Environment variables from workbench selection
46/// * `feature_env` - Environment variables from feature flags
47/// * `script_env` - Environment variables from Rhai script
48/// * `preserve_current` - Whether to preserve current process environment
49///
50/// # Returns
51///
52/// Final resolved HashMap of environment variables
53///
54/// # Example
55///
56/// ```no_run
57/// use crate::Maintain::Source::Build::Rhai::EnvironmentResolver;
58///
59/// let templates = HashMap::from([("PATH", "/usr/bin".to_string())]);
60/// let profile = HashMap::from([("NODE_ENV", "development".to_string())]);
61/// let workbench = HashMap::from([("Mountain", "true".to_string())]);
62/// let features = HashMap::from([("FEATURE_TAURI_IPC", "true".to_string())]);
63/// let script = HashMap::from([("RUST_LOG", "debug".to_string())]);
64///
65/// let final_env = EnvironmentResolver::resolve_full(
66/// templates, profile, workbench, features, script, true
67/// );
68/// ```
69pub fn resolve_full(
70 template_env: HashMap<String, String>,
71 profile_env: HashMap<String, String>,
72 workbench_env: HashMap<String, String>,
73 feature_env: HashMap<String, String>,
74 script_env: HashMap<String, String>,
75 preserve_current: bool,
76) -> HashMap<String, String> {
77 let mut resolved = HashMap::new();
78
79 // Start with current environment if requested
80 if preserve_current {
81 for (key, value) in std::env::vars_os() {
82 if let (Ok(key_str), Ok(value_str)) = (key.into_string(), value.into_string()) {
83 resolved.insert(key_str, value_str);
84 }
85 }
86 }
87
88 // Apply template values (lowest priority)
89 for (key, value) in template_env {
90 resolved.insert(key, value);
91 }
92
93 // Apply profile values (overriding templates)
94 for (key, value) in profile_env {
95 resolved.insert(key, value);
96 }
97
98 // Apply workbench values (overriding profile)
99 for (key, value) in workbench_env {
100 resolved.insert(key, value);
101 }
102
103 // Apply feature flag values
104 for (key, value) in feature_env {
105 resolved.insert(key, value);
106 }
107
108 // Apply script values (highest priority)
109 for (key, value) in script_env {
110 resolved.insert(key, value);
111 }
112
113 // Apply environment variable prefixes per crate
114 apply_prefixes(&mut resolved);
115
116 // Expand variable references
117 expand_variables(&mut resolved);
118
119 resolved
120}
121
122/// Resolves the final set of environment variables (simplified version).
123///
124/// This function merges variables from:
125/// 1. Template defaults
126/// 2. Profile-specific static variables
127/// 3. Rhai script output
128/// 4. Current process environment
129///
130/// # Arguments
131///
132/// * `template_env` - Environment variables from templates
133/// * `profile_env` - Environment variables from profile
134/// * `script_env` - Environment variables from Rhai script
135/// * `preserve_current` - Whether to preserve current process environment
136///
137/// # Returns
138///
139/// Final resolved HashMap of environment variables
140pub fn resolve(
141 template_env: HashMap<String, String>,
142 profile_env: HashMap<String, String>,
143 script_env: HashMap<String, String>,
144 preserve_current: bool,
145) -> HashMap<String, String> {
146 resolve_full(
147 template_env,
148 profile_env,
149 HashMap::new(),
150 HashMap::new(),
151 script_env,
152 preserve_current,
153 )
154}
155
156/// Applies environment variables to the current process.
157///
158/// # Arguments
159///
160/// * `env_vars` - Environment variables to apply
161///
162/// # Example
163///
164/// ```no_run
165/// use crate::Maintain::Source::Build::Rhai::EnvironmentResolver;
166///
167/// let env = HashMap::from([
168/// ("NODE_ENV".to_string(), "production".to_string()),
169/// ("RUST_LOG".to_string(), "info".to_string()),
170/// ]);
171///
172/// EnvironmentResolver::apply(&env);
173/// ```
174pub fn apply(env_vars: &HashMap<String, String>) {
175 for (key, value) in env_vars {
176 // Safety: set_var is now unsafe in recent Rust versions
177 // Setting environment variables during build orchestration is acceptable
178 // as it doesn't violate memory safety.
179 unsafe { std::env::set_var(key, value); }
180 }
181}
182
183/// Converts environment variables to a formatted string for logging.
184///
185/// # Arguments
186///
187/// * `env_vars` - Environment variables to format
188///
189/// # Returns
190///
191/// Formatted string representation
192pub fn format_env(env_vars: &HashMap<String, String>) -> String {
193 let mut entries: Vec<_> = env_vars.iter().collect();
194 entries.sort_by_key(|(k, _)| *k);
195
196 entries
197 .iter()
198 .map(|(k, v)| format!(" {}={}", k, v))
199 .collect::<Vec<_>>()
200 .join("\n")
201}
202
203/// Validates that required environment variables are set.
204///
205/// # Arguments
206///
207/// * `env_vars` - Current environment variables
208/// * `required` - List of required variable names
209///
210/// # Returns
211///
212/// Result indicating success or list of missing variables
213pub fn validate_required(
214 env_vars: &HashMap<String, String>,
215 required: &[&str],
216) -> Result<(), Vec<String>> {
217 let missing: Vec<String> = required
218 .iter()
219 .filter(|var| !env_vars.contains_key(&var.to_string()))
220 .map(|s| s.to_string())
221 .collect();
222
223 if missing.is_empty() {
224 Ok(())
225 } else {
226 Err(missing)
227 }
228}
229
230/// Generates workbench-specific environment variables.
231///
232/// # Arguments
233///
234/// * `workbench_type` - The selected workbench type
235///
236/// # Returns
237///
238/// HashMap of workbench environment variables
239pub fn generate_workbench_env(workbench_type: &str) -> HashMap<String, String> {
240 let mut env = HashMap::new();
241
242 // Set the workbench type as an environment variable
243 env.insert(workbench_type.to_string(), "true".to_string());
244
245 // Set WORKBENCH_TYPE for use in build scripts
246 env.insert("WORKBENCH_TYPE".to_string(), workbench_type.to_string());
247
248 env
249}
250
251/// Generates feature flag environment variables from a feature map.
252///
253/// # Arguments
254///
255/// * `features` - HashMap of feature name to enabled status
256///
257/// # Returns
258///
259/// HashMap of FEATURE_* environment variables
260pub fn generate_feature_env(features: &HashMap<String, bool>) -> HashMap<String, String> {
261 features
262 .iter()
263 .map(|(name, value)| {
264 let env_key = format!("FEATURE_{}", name.to_uppercase().replace('-', "_"));
265 (env_key, value.to_string())
266 })
267 .collect()
268}
269
270//=============================================================================
271// Private Helper Functions
272//=============================================================================
273
274/// Applies environment variable prefixes per crate.
275fn apply_prefixes(_env_vars: &mut HashMap<String, String>) {
276 // Define known crate prefixes
277 let prefixes = [
278 ("air", "AIR_"),
279 ("cocoon", "MOUNTAIN_"),
280 ("grove", "VSCODE_"),
281 ("maintain", "LAND_"),
282 ];
283
284 // For now, this is a no-op as prefixes are handled in the config
285 // Future: could auto-prefix variables based on their names
286 let _ = prefixes;
287}
288
289/// Expands variable references in environment variable values.
290///
291/// Supports ${VAR} syntax for variable expansion.
292fn expand_variables(env_vars: &mut HashMap<String, String>) {
293 // Collect all current values for reference
294 let original: HashMap<String, String> = env_vars.clone();
295
296 // Expand ${VAR} references in each value
297 for value in env_vars.values_mut() {
298 // Simple expansion - replace ${VAR} with the value from original
299 let mut expanded = value.clone();
300 let mut start = 0;
301
302 while let Some(open) = expanded[start..].find("${") {
303 let abs_open = start + open;
304 if let Some(close) = expanded[abs_open..].find('}') {
305 let var_name = &expanded[abs_open + 2..abs_open + close];
306 if let Some(replacement) = original.get(var_name) {
307 expanded.replace_range(abs_open..abs_open + close + 1, replacement);
308 // Continue from after the replacement
309 start = abs_open + replacement.len();
310 } else {
311 // Variable not found, skip past this reference
312 start = abs_open + close + 1;
313 }
314 } else {
315 break;
316 }
317 }
318
319 *value = expanded;
320 }
321}
322
323//=============================================================================
324// Tests
325//=============================================================================
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_resolve() {
333 let template = HashMap::from([("A", "1".to_string()), ("B", "2".to_string())]);
334 let profile = HashMap::from([("B", "3".to_string()), ("C", "4".to_string())]);
335 let script = HashMap::from([("C", "5".to_string())]);
336
337 let result = resolve(template, profile, script, false);
338
339 assert_eq!(result.get("A"), Some(&"1".to_string())); // From template
340 assert_eq!(result.get("B"), Some(&"3".to_string())); // Profile overrides template
341 assert_eq!(result.get("C"), Some(&"5".to_string())); // Script overrides profile
342 }
343
344 #[test]
345 fn test_generate_workbench_env() {
346 let env = generate_workbench_env("Mountain");
347
348 assert_eq!(env.get("Mountain"), Some(&"true".to_string()));
349 assert_eq!(env.get("WORKBENCH_TYPE"), Some(&"Mountain".to_string()));
350 }
351
352 #[test]
353 fn test_generate_feature_env() {
354 let mut features = HashMap::new();
355 features.insert("tauri-ipc".to_string(), true);
356 features.insert("wind-services".to_string(), false);
357
358 let env = generate_feature_env(&features);
359
360 assert_eq!(env.get("FEATURE_TAURI_IPC"), Some(&"true".to_string()));
361 assert_eq!(env.get("FEATURE_WIND_SERVICES"), Some(&"false".to_string()));
362 }
363
364 #[test]
365 fn test_validate_required() {
366 let env = HashMap::from([
367 ("A".to_string(), "1".to_string()),
368 ("B".to_string(), "2".to_string()),
369 ]);
370
371 assert!(validate_required(&env, &["A", "B"]).is_ok());
372 assert!(validate_required(&env, &["A", "C"]).is_err());
373 }
374
375 #[test]
376 fn test_expand_variables() {
377 let mut env = HashMap::new();
378 env.insert("BASE".to_string(), "/path/to/base".to_string());
379 env.insert("FULL".to_string(), "${BASE}/sub".to_string());
380
381 expand_variables(&mut env);
382
383 assert_eq!(env.get("FULL"), Some(&"/path/to/base/sub".to_string()));
384 }
385}