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}