Skip to main content

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 std::{collections::HashMap, path::Path};
25
26use serde::Deserialize;
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).join(".vscode").join("land-config.json");
330
331	load_config(&config_path)
332}
333
334/// Loads the land-config.json file from a specific path.
335///
336/// # Arguments
337///
338/// * `config_path` - Path to the configuration file
339///
340/// # Returns
341///
342/// Result containing the parsed LandConfig or an error
343///
344/// # Example
345///
346/// ```no_run
347/// use crate::Maintain::Source::Build::Rhai::load_config;
348/// let config = load_config(".vscode/land-config.json")?;
349/// let debug_profile = config.profiles.get("debug");
350/// ```
351pub fn load_config(config_path:&Path) -> Result<LandConfig, String> {
352	if !config_path.exists() {
353		return Err(format!("Configuration file not found: {}", config_path.display()));
354	}
355
356	let content = std::fs::read_to_string(config_path).map_err(|e| format!("Failed to read config file: {}", e))?;
357
358	// Parse JSON5 (using json5 crate for comment support)
359	let config:LandConfig = json5::from_str(&content).map_err(|e| format!("Failed to parse config JSON: {}", e))?;
360
361	Ok(config)
362}
363
364/// Gets a specific profile by name.
365///
366/// # Arguments
367///
368/// * `config` - The loaded configuration
369/// * `profile_name` - Name of the profile to retrieve
370///
371/// # Returns
372///
373/// Option containing the profile if found
374pub fn get_profile<'a>(config:&'a LandConfig, profile_name:&str) -> Option<&'a Profile> {
375	config.profiles.get(profile_name)
376}
377
378/// Gets the workbench type for a profile.
379///
380/// # Arguments
381///
382/// * `config` - The loaded configuration
383/// * `profile_name` - Name of the profile
384///
385/// # Returns
386///
387/// The workbench type for the profile, or the default workbench
388pub fn get_workbench_type(config:&LandConfig, profile_name:&str) -> String {
389	if let Some(profile) = config.profiles.get(profile_name) {
390		if let Some(workbench) = &profile.workbench {
391			return workbench.clone();
392		}
393	}
394
395	// Return default workbench from config
396	if let Some(workbench_config) = &config.workbench {
397		if let Some(default) = &workbench_config.default {
398			return default.clone();
399		}
400	}
401
402	// Fallback to Browser
403	"Browser".to_string()
404}
405
406/// Gets the features for a workbench type.
407///
408/// # Arguments
409///
410/// * `config` - The loaded configuration
411/// * `workbench_type` - The workbench type
412///
413/// # Returns
414///
415/// Option containing the workbench features if found
416pub fn get_workbench_features<'a>(config:&'a LandConfig, workbench_type:&str) -> Option<&'a WorkbenchFeatures> {
417	if let Some(workbench_config) = &config.workbench {
418		if let Some(features) = &workbench_config.features {
419			return features.get(workbench_type);
420		}
421	}
422	None
423}
424
425/// Resolves all environment variables for a profile.
426///
427/// This merges template variables with profile-specific variables,
428/// with profile variables taking precedence.
429///
430/// # Arguments
431///
432/// * `config` - The loaded configuration
433/// * `profile_name` - Name of the profile to resolve
434///
435/// # Returns
436///
437/// HashMap of all environment variables for the profile
438pub fn resolve_profile_env(config:&LandConfig, profile_name:&str) -> HashMap<String, String> {
439	let mut env_vars = HashMap::new();
440
441	// Start with template values
442	if let Some(templates) = &config.templates {
443		for (key, value) in &templates.env {
444			env_vars.insert(key.clone(), value.clone());
445		}
446	}
447
448	// Apply profile-specific values (overriding templates)
449	if let Some(profile) = config.profiles.get(profile_name) {
450		if let Some(profile_env) = &profile.env {
451			for (key, value) in profile_env {
452				env_vars.insert(key.clone(), value.clone());
453			}
454		}
455	}
456
457	// Add workbench environment variable based on profile workbench type
458	let workbench_type = get_workbench_type(config, profile_name);
459	env_vars.insert(workbench_type.clone(), "true".to_string());
460
461	env_vars
462}
463
464/// Resolves all feature flags for a profile.
465///
466/// This merges default feature values with profile-specific overrides.
467///
468/// # Arguments
469///
470/// * `config` - The loaded configuration
471/// * `profile_name` - Name of the profile to resolve
472///
473/// # Returns
474///
475/// HashMap of all feature flags for the profile
476pub fn resolve_profile_features(config:&LandConfig, profile_name:&str) -> HashMap<String, bool> {
477	let mut features = HashMap::new();
478
479	// Start with default feature values
480	if let Some(feature_config) = &config.features {
481		for (name, config) in feature_config {
482			features.insert(name.clone(), config.default.unwrap_or(false));
483		}
484	}
485
486	// Apply profile-specific feature overrides
487	if let Some(profile) = config.profiles.get(profile_name) {
488		if let Some(profile_features) = &profile.features {
489			for (key, value) in profile_features {
490				features.insert(key.clone(), *value);
491			}
492		}
493	}
494
495	features
496}
497
498/// Generates environment variables from feature flags.
499///
500/// Converts feature flags to FEATURE_* environment variables.
501///
502/// # Arguments
503///
504/// * `features` - HashMap of feature flags
505///
506/// # Returns
507///
508/// HashMap of FEATURE_* environment variables
509pub fn features_to_env(features:&HashMap<String, bool>) -> HashMap<String, String> {
510	let mut env_vars = HashMap::new();
511
512	for (name, value) in features {
513		let env_key = format!("FEATURE_{}", name.to_uppercase().replace("-", "_"));
514		env_vars.insert(env_key, value.to_string());
515	}
516
517	env_vars
518}
519
520/// Gets the build command for a profile.
521///
522/// # Arguments
523///
524/// * `config` - The loaded configuration
525/// * `profile_name` - Name of the profile
526///
527/// # Returns
528///
529/// Option containing the build command if found
530pub fn get_build_command(config:&LandConfig, profile_name:&str) -> Option<String> {
531	config.build_commands.as_ref()?.get(profile_name).cloned()
532}
533
534//=============================================================================
535// Tests
536//=============================================================================
537
538#[cfg(test)]
539mod tests {
540	use super::*;
541
542	#[test]
543	fn test_load_config() {
544		// This test would require a test fixture config file
545		// For now, we just verify the types compile correctly
546	}
547
548	#[test]
549	fn test_features_to_env() {
550		let mut features = HashMap::new();
551		features.insert("tauri_ipc".to_string(), true);
552		features.insert("wind_services".to_string(), false);
553
554		let env = features_to_env(&features);
555
556		assert_eq!(env.get("FEATURE_TAURI_IPC"), Some(&"true".to_string()));
557		assert_eq!(env.get("FEATURE_WIND_SERVICES"), Some(&"false".to_string()));
558	}
559}