Maintain/Build/Rhai/
ConfigLoader.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/Rhai/ConfigLoader.rs
3//=============================================================================//
4// Module: ConfigLoader
5//
6// Brief Description: Loads and parses the land-config.json configuration file.
7//
8// RESPONSIBILITIES:
9// ================
10//
11// Primary:
12// - Load the JSON5 configuration file
13// - Parse profiles, templates, workbench, features, and build commands
14// - Validate configuration structure
15// - Provide fast access to configuration data
16//
17// Secondary:
18// - Cache parsed configuration for performance
19// - Handle configuration errors gracefully
20// - Support workbench and feature flag resolution
21//
22//=============================================================================//
23
24use serde::Deserialize;
25use std::collections::HashMap;
26use std::path::Path;
27
28//=============================================================================
29// Configuration Types
30//=============================================================================
31
32/// The main configuration structure loaded from land-config.json
33#[derive(Debug, Deserialize, Clone)]
34pub struct LandConfig {
35	/// Configuration version
36	pub version: String,
37	/// Workbench configuration
38	pub workbench: Option<WorkbenchConfig>,
39	/// Feature flags configuration
40	pub features: Option<HashMap<String, FeatureConfig>>,
41	/// Binary configuration
42	pub binary: Option<BinaryConfig>,
43	/// Build profiles (debug, production, release, etc.)
44	pub profiles: HashMap<String, Profile>,
45	/// Default template values
46	pub templates: Option<Templates>,
47	/// Environment variable prefixes per crate
48	#[serde(rename = "env_prefixes")]
49	pub env_prefixes: Option<HashMap<String, String>>,
50	/// Build command templates
51	#[serde(rename = "build_commands")]
52	pub build_commands: Option<HashMap<String, String>>,
53	/// Environment variable inventory
54	#[serde(rename = "environment_variables")]
55	pub environment_variables: Option<EnvironmentVariableInventory>,
56	/// CLI configuration
57	pub cli: Option<CliConfig>,
58}
59
60/// CLI configuration settings
61#[derive(Debug, Deserialize, Clone)]
62pub struct CliConfig {
63	/// Default profile to use
64	#[serde(rename = "default_profile")]
65	pub default_profile: Option<String>,
66	/// Configuration file path
67	#[serde(rename = "config_file")]
68	pub config_file: Option<String>,
69	/// Log format
70	#[serde(rename = "log_format")]
71	pub log_format: Option<String>,
72	/// Enable colors
73	pub colors: Option<bool>,
74	/// Show progress
75	pub progress: Option<bool>,
76	/// Dry run default
77	#[serde(rename = "dry_run_default")]
78	pub dry_run_default: Option<bool>,
79	/// Profile aliases
80	#[serde(rename = "profile_aliases")]
81	pub profile_aliases: HashMap<String, String>,
82}
83
84/// Environment variable inventory structure
85#[derive(Debug, Deserialize, Clone)]
86pub struct EnvironmentVariableInventory {
87	/// Build flags
88	#[serde(rename = "build_flags")]
89	pub build_flags: Option<HashMap<String, EnvironmentVariableInfo>>,
90	/// Build configuration
91	#[serde(rename = "build_config")]
92	pub build_config: Option<HashMap<String, EnvironmentVariableInfo>>,
93	/// Node.js configuration
94	pub node: Option<HashMap<String, EnvironmentVariableInfo>>,
95	/// Rust configuration
96	pub rust: Option<HashMap<String, EnvironmentVariableInfo>>,
97	/// Mountain configuration
98	pub mountain: Option<HashMap<String, EnvironmentVariableInfo>>,
99	/// Tauri configuration
100	pub tauri: Option<HashMap<String, EnvironmentVariableInfo>>,
101	/// Apple signing configuration
102	pub apple: Option<HashMap<String, EnvironmentVariableInfo>>,
103	/// Android configuration
104	pub android: Option<HashMap<String, EnvironmentVariableInfo>>,
105	/// CI/CD configuration
106	pub ci: Option<HashMap<String, EnvironmentVariableInfo>>,
107	/// API configuration
108	pub api: Option<HashMap<String, EnvironmentVariableInfo>>,
109	/// Other configuration
110	pub other: Option<HashMap<String, EnvironmentVariableInfo>>,
111}
112
113/// Environment variable information
114#[derive(Debug, Deserialize, Clone)]
115pub struct EnvironmentVariableInfo {
116	/// Variable type
117	#[serde(rename = "type")]
118	pub var_type: Option<String>,
119	/// Description
120	pub description: Option<String>,
121	/// Allowed values
122	pub values: Option<Vec<String>>,
123	/// Default value
124	pub default: Option<String>,
125	/// Configuration path
126	#[serde(rename = "config_path")]
127	pub config_path: Option<String>,
128	/// Whether this is sensitive
129	pub sensitive: Option<bool>,
130}
131
132/// Workbench configuration
133#[derive(Debug, Deserialize, Clone)]
134pub struct WorkbenchConfig {
135	/// Default workbench type
136	pub default: Option<String>,
137	/// Available workbench types
138	pub available: Option<Vec<String>>,
139	/// Feature sets per workbench
140	pub features: Option<HashMap<String, WorkbenchFeatures>>,
141}
142
143/// Features for a specific workbench
144#[derive(Debug, Deserialize, Clone)]
145pub struct WorkbenchFeatures {
146	/// Human-readable description
147	pub description: Option<String>,
148	/// Feature coverage percentage
149	pub coverage: Option<String>,
150	/// Complexity level
151	pub complexity: Option<String>,
152	/// Whether this workbench requires polyfills
153	pub polyfills: Option<bool>,
154	/// Whether this workbench uses Mountain providers
155	#[serde(rename = "mountain_providers")]
156	pub mountain_providers: Option<bool>,
157	/// Whether this workbench uses Wind services
158	#[serde(rename = "wind_services")]
159	pub wind_services: Option<bool>,
160	/// Whether this workbench uses Electron APIs
161	#[serde(rename = "electron_apis")]
162	pub electron_apis: Option<bool>,
163	/// Whether this workbench is recommended
164	pub recommended: Option<bool>,
165	/// Recommended use cases
166	#[serde(rename = "recommended_for")]
167	pub recommended_for: Option<Vec<String>>,
168}
169
170/// Feature flag configuration
171#[derive(Debug, Deserialize, Clone)]
172pub struct FeatureConfig {
173	/// Human-readable description
174	pub description: Option<String>,
175	/// Default value
176	pub default: Option<bool>,
177	/// Dependencies
178	#[serde(rename = "depends_on")]
179	pub depends_on: Option<Vec<String>>,
180}
181
182/// Binary configuration
183#[derive(Debug, Deserialize, Clone)]
184pub struct BinaryConfig {
185	/// Binary name template
186	#[serde(rename = "name_template")]
187	pub name_template: Option<String>,
188	/// Binary identifier template
189	#[serde(rename = "identifier_template")]
190	pub identifier_template: Option<String>,
191	/// Version format
192	#[serde(rename = "version_format")]
193	pub version_format: Option<String>,
194	/// Signing configuration
195	pub sign: Option<SignConfig>,
196	/// Notarization configuration
197	pub notarize: Option<NotarizeConfig>,
198	/// Updater configuration
199	pub updater: Option<UpdaterConfig>,
200}
201
202/// Signing configuration
203#[derive(Debug, Deserialize, Clone)]
204pub struct SignConfig {
205	/// macOS signing settings
206	pub macos: Option<MacOSSignConfig>,
207	/// Windows signing settings
208	pub windows: Option<WindowsSignConfig>,
209	/// Linux signing settings
210	pub linux: Option<LinuxSignConfig>,
211}
212
213/// macOS signing configuration
214#[derive(Debug, Deserialize, Clone)]
215pub struct MacOSSignConfig {
216	/// Signing identity
217	pub identity: Option<String>,
218	/// Entitlements file path
219	pub entitlements: Option<String>,
220	/// Enable hardened runtime
221	#[serde(rename = "hardenedRuntime")]
222	pub hardened_runtime: Option<bool>,
223	/// Gatekeeper assessment
224	#[serde(rename = "gatekeeper_assess")]
225	pub gatekeeper_assess: Option<bool>,
226}
227
228/// Windows signing configuration
229#[derive(Debug, Deserialize, Clone)]
230pub struct WindowsSignConfig {
231	/// Certificate path
232	pub certificate: Option<String>,
233	/// Timestamp server
234	#[serde(rename = "timestamp_server")]
235	pub timestamp_server: Option<String>,
236	/// TSA URL restrictions
237	#[serde(rename = "tsa_can_only_access_urls")]
238	pub tsa_can_only_access_urls: Option<Vec<String>>,
239}
240
241/// Linux signing configuration
242#[derive(Debug, Deserialize, Clone)]
243pub struct LinuxSignConfig {
244	/// GPG key
245	#[serde(rename = "gpg_key")]
246	pub gpg_key: Option<String>,
247	/// GPG passphrase environment variable
248	#[serde(rename = "gpg_passphrase_env")]
249	pub gpg_passphrase_env: Option<String>,
250}
251
252/// Notarization configuration
253#[derive(Debug, Deserialize, Clone)]
254pub struct NotarizeConfig {
255	/// macOS notarization settings
256	pub macos: Option<MacOSNotarizeConfig>,
257}
258
259/// macOS notarization configuration
260#[derive(Debug, Deserialize, Clone)]
261pub struct MacOSNotarizeConfig {
262	/// Apple ID
263	#[serde(rename = "apple_id")]
264	pub apple_id: Option<String>,
265	/// Password environment variable
266	#[serde(rename = "password_env")]
267	pub password_env: Option<String>,
268	/// Team ID
269	#[serde(rename = "team_id")]
270	pub team_id: Option<String>,
271}
272
273/// Updater configuration
274#[derive(Debug, Deserialize, Clone)]
275pub struct UpdaterConfig {
276	/// Enable updater
277	pub enabled: Option<bool>,
278	/// Update endpoints
279	pub endpoints: Option<Vec<String>>,
280	/// Public key
281	pub pubkey: Option<String>,
282}
283
284/// A build profile configuration
285#[derive(Debug, Deserialize, Clone)]
286pub struct Profile {
287	/// Human-readable description
288	pub description: Option<String>,
289	/// Workbench type for this profile
290	pub workbench: Option<String>,
291	/// Static environment variables for this profile
292	pub env: Option<HashMap<String, String>>,
293	/// Feature flags for this profile
294	pub features: Option<HashMap<String, bool>>,
295	/// Path to Rhai script for this profile
296	#[serde(rename = "rhai_script")]
297	pub rhai_script: Option<String>,
298}
299
300/// Default template values used across profiles
301#[derive(Debug, Deserialize, Clone)]
302pub struct Templates {
303	/// Default environment variables
304	pub env: HashMap<String, String>,
305}
306
307//=============================================================================
308// Public API
309//=============================================================================
310
311/// Loads the land-config.json file from the .vscode directory.
312///
313/// # Arguments
314///
315/// * `workspace_root` - Path to the workspace root directory
316///
317/// # Returns
318///
319/// Result containing the parsed LandConfig or an error
320///
321/// # Example
322///
323/// ```no_run
324/// use crate::Maintain::Source::Build::Rhai::ConfigLoader;
325/// let config = ConfigLoader::load(".")?;
326/// let debug_profile = config.profiles.get("debug");
327/// ```
328pub fn load(workspace_root: &str) -> Result<LandConfig, String> {
329	let config_path = Path::new(workspace_root)
330		.join(".vscode")
331		.join("land-config.json");
332
333	load_config(&config_path)
334}
335
336/// Loads the land-config.json file from a specific path.
337///
338/// # Arguments
339///
340/// * `config_path` - Path to the configuration file
341///
342/// # Returns
343///
344/// Result containing the parsed LandConfig or an error
345///
346/// # Example
347///
348/// ```no_run
349/// use crate::Maintain::Source::Build::Rhai::load_config;
350/// let config = load_config(".vscode/land-config.json")?;
351/// let debug_profile = config.profiles.get("debug");
352/// ```
353pub fn load_config(config_path: &Path) -> Result<LandConfig, String> {
354	if !config_path.exists() {
355		return Err(format!(
356			"Configuration file not found: {}",
357			config_path.display()
358		));
359	}
360
361	let content = std::fs::read_to_string(config_path)
362		.map_err(|e| format!("Failed to read config file: {}", e))?;
363
364	// Parse JSON5 (using json5 crate for comment support)
365	let config: LandConfig = json5::from_str(&content)
366		.map_err(|e| format!("Failed to parse config JSON: {}", e))?;
367
368	Ok(config)
369}
370
371/// Gets a specific profile by name.
372///
373/// # Arguments
374///
375/// * `config` - The loaded configuration
376/// * `profile_name` - Name of the profile to retrieve
377///
378/// # Returns
379///
380/// Option containing the profile if found
381pub fn get_profile<'a>(config: &'a LandConfig, profile_name: &str) -> Option<&'a Profile> {
382	config.profiles.get(profile_name)
383}
384
385/// Gets the workbench type for a profile.
386///
387/// # Arguments
388///
389/// * `config` - The loaded configuration
390/// * `profile_name` - Name of the profile
391///
392/// # Returns
393///
394/// The workbench type for the profile, or the default workbench
395pub fn get_workbench_type(config: &LandConfig, profile_name: &str) -> String {
396	if let Some(profile) = config.profiles.get(profile_name) {
397		if let Some(workbench) = &profile.workbench {
398			return workbench.clone();
399		}
400	}
401
402	// Return default workbench from config
403	if let Some(workbench_config) = &config.workbench {
404		if let Some(default) = &workbench_config.default {
405			return default.clone();
406		}
407	}
408
409	// Fallback to Browser
410	"Browser".to_string()
411}
412
413/// Gets the features for a workbench type.
414///
415/// # Arguments
416///
417/// * `config` - The loaded configuration
418/// * `workbench_type` - The workbench type
419///
420/// # Returns
421///
422/// Option containing the workbench features if found
423pub fn get_workbench_features<'a>(
424	config: &'a LandConfig,
425	workbench_type: &str,
426) -> Option<&'a WorkbenchFeatures> {
427	if let Some(workbench_config) = &config.workbench {
428		if let Some(features) = &workbench_config.features {
429			return features.get(workbench_type);
430		}
431	}
432	None
433}
434
435/// Resolves all environment variables for a profile.
436///
437/// This merges template variables with profile-specific variables,
438/// with profile variables taking precedence.
439///
440/// # Arguments
441///
442/// * `config` - The loaded configuration
443/// * `profile_name` - Name of the profile to resolve
444///
445/// # Returns
446///
447/// HashMap of all environment variables for the profile
448pub fn resolve_profile_env(config: &LandConfig, profile_name: &str) -> HashMap<String, String> {
449	let mut env_vars = HashMap::new();
450
451	// Start with template values
452	if let Some(templates) = &config.templates {
453		for (key, value) in &templates.env {
454			env_vars.insert(key.clone(), value.clone());
455		}
456	}
457
458	// Apply profile-specific values (overriding templates)
459	if let Some(profile) = config.profiles.get(profile_name) {
460		if let Some(profile_env) = &profile.env {
461			for (key, value) in profile_env {
462				env_vars.insert(key.clone(), value.clone());
463			}
464		}
465	}
466
467	// Add workbench environment variable based on profile workbench type
468	let workbench_type = get_workbench_type(config, profile_name);
469	env_vars.insert(workbench_type.clone(), "true".to_string());
470
471	env_vars
472}
473
474/// Resolves all feature flags for a profile.
475///
476/// This merges default feature values with profile-specific overrides.
477///
478/// # Arguments
479///
480/// * `config` - The loaded configuration
481/// * `profile_name` - Name of the profile to resolve
482///
483/// # Returns
484///
485/// HashMap of all feature flags for the profile
486pub fn resolve_profile_features(config: &LandConfig, profile_name: &str) -> HashMap<String, bool> {
487	let mut features = HashMap::new();
488
489	// Start with default feature values
490	if let Some(feature_config) = &config.features {
491		for (name, config) in feature_config {
492			features.insert(name.clone(), config.default.unwrap_or(false));
493		}
494	}
495
496	// Apply profile-specific feature overrides
497	if let Some(profile) = config.profiles.get(profile_name) {
498		if let Some(profile_features) = &profile.features {
499			for (key, value) in profile_features {
500				features.insert(key.clone(), *value);
501			}
502		}
503	}
504
505	features
506}
507
508/// Generates environment variables from feature flags.
509///
510/// Converts feature flags to FEATURE_* environment variables.
511///
512/// # Arguments
513///
514/// * `features` - HashMap of feature flags
515///
516/// # Returns
517///
518/// HashMap of FEATURE_* environment variables
519pub fn features_to_env(features: &HashMap<String, bool>) -> HashMap<String, String> {
520	let mut env_vars = HashMap::new();
521
522	for (name, value) in features {
523		let env_key = format!("FEATURE_{}", name.to_uppercase().replace("-", "_"));
524		env_vars.insert(env_key, value.to_string());
525	}
526
527	env_vars
528}
529
530/// Gets the build command for a profile.
531///
532/// # Arguments
533///
534/// * `config` - The loaded configuration
535/// * `profile_name` - Name of the profile
536///
537/// # Returns
538///
539/// Option containing the build command if found
540pub fn get_build_command(config: &LandConfig, profile_name: &str) -> Option<String> {
541	config.build_commands.as_ref()?.get(profile_name).cloned()
542}
543
544//=============================================================================
545// Tests
546//=============================================================================
547
548#[cfg(test)]
549mod tests {
550	use super::*;
551
552	#[test]
553	fn test_load_config() {
554		// This test would require a test fixture config file
555		// For now, we just verify the types compile correctly
556	}
557
558	#[test]
559	fn test_features_to_env() {
560		let mut features = HashMap::new();
561		features.insert("tauri_ipc".to_string(), true);
562		features.insert("wind_services".to_string(), false);
563
564		let env = features_to_env(&features);
565
566		assert_eq!(env.get("FEATURE_TAURI_IPC"), Some(&"true".to_string()));
567		assert_eq!(env.get("FEATURE_WIND_SERVICES"), Some(&"false".to_string()));
568	}
569}