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