envx_core/
project_manager.rs

1use crate::project_config::ProjectConfig;
2use crate::{EnvVarManager, ProfileManager, ValidationRules};
3use ahash::AHashMap as HashMap;
4use color_eyre::Result;
5use color_eyre::eyre::eyre;
6use regex::Regex;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10pub struct ProjectManager {
11    config_dir: PathBuf,
12    config: Option<ProjectConfig>,
13    current_dir: PathBuf,
14}
15
16impl ProjectManager {
17    /// Create a new `ProjectManager` instance
18    ///
19    /// # Errors
20    ///
21    /// This function will return an error if getting the current directory fails.
22    pub fn new() -> Result<Self> {
23        Ok(Self {
24            config_dir: PathBuf::from(".envx"),
25            config: None,
26            current_dir: std::env::current_dir()?,
27        })
28    }
29
30    /// Initialize a new project configuration
31    ///
32    /// # Errors
33    ///
34    /// This function will return an error if:
35    /// - Creating the .envx directory fails
36    /// - Saving the configuration file fails
37    /// - Writing the .gitignore file fails
38    pub fn init(&self, name: Option<String>) -> Result<()> {
39        // Create .envx directory
40        fs::create_dir_all(&self.config_dir)?;
41
42        // Create config.yaml
43        let config = ProjectConfig::new(name);
44        let config_path = self.config_dir.join("config.yaml");
45        config.save(&config_path)?;
46
47        // Create .gitignore for .envx directory
48        let gitignore_path = self.config_dir.join(".gitignore");
49        fs::write(gitignore_path, "local/\n*.local.yaml\n")?;
50
51        println!("✅ Initialized envx project configuration");
52        println!("📁 Created .envx/config.yaml");
53
54        Ok(())
55    }
56
57    /// Initialize a new project with a custom configuration file
58    ///
59    /// # Errors
60    ///
61    /// This function will return an error if:
62    /// - Creating parent directories fails
63    /// - Saving the configuration file fails
64    pub fn init_with_file(&self, name: Option<String>, file_path: &Path) -> Result<()> {
65        // Create parent directories if needed
66        if let Some(parent) = file_path.parent() {
67            fs::create_dir_all(parent)?;
68        }
69
70        let project_name = name
71            .or_else(|| {
72                std::env::current_dir()
73                    .ok()
74                    .and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()))
75            })
76            .unwrap_or_else(|| "my-project".to_string());
77
78        let config = ProjectConfig {
79            name: Some(project_name.clone()),
80            description: Some(format!("{project_name} environment configuration")),
81            required: vec![],
82            defaults: HashMap::new(),
83            auto_load: vec![".env".to_string()],
84            profile: None,
85            scripts: HashMap::new(),
86            validation: ValidationRules::default(),
87            inherit: true,
88        };
89
90        config.save(file_path)?;
91        println!("✅ Initialized project '{}' at {}", project_name, file_path.display());
92
93        Ok(())
94    }
95
96    /// Find and load project configuration
97    ///
98    /// # Errors
99    ///
100    /// This function will return an error if loading the project configuration file fails.
101    pub fn find_and_load(&mut self) -> Result<Option<PathBuf>> {
102        let mut current = self.current_dir.clone();
103
104        loop {
105            let config_path = current.join(".envx").join("config.yaml");
106            if config_path.exists() {
107                self.config = Some(ProjectConfig::load(&config_path)?);
108                return Ok(Some(current));
109            }
110
111            if !current.pop() {
112                break;
113            }
114        }
115
116        Ok(None)
117    }
118
119    /// Apply project configuration
120    ///
121    /// # Errors
122    ///
123    /// This function will return an error if:
124    /// - No project configuration is loaded
125    /// - Profile application fails
126    /// - Loading environment files fails
127    /// - Setting environment variables fails
128    pub fn apply(&self, manager: &mut EnvVarManager, profile_manager: &mut ProfileManager) -> Result<()> {
129        let config = self
130            .config
131            .as_ref()
132            .ok_or_else(|| color_eyre::eyre::eyre!("No project configuration loaded"))?;
133
134        // Apply profile if specified
135        if let Some(profile_name) = &config.profile {
136            profile_manager.apply(profile_name, manager)?;
137        }
138
139        // Load auto-load files
140        for file in &config.auto_load {
141            let file_path = self.current_dir.join(file);
142            if file_path.exists() {
143                Self::load_env_file(&file_path, manager)?;
144            }
145        }
146
147        // Apply defaults (only if variable not already set)
148        for (name, value) in &config.defaults {
149            if manager.get(name).is_none() {
150                manager.set(name, value, true)?;
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Load configuration from a specific file
158    ///
159    /// # Errors
160    ///
161    /// This function will return an error if:
162    /// - The configuration file does not exist
163    /// - Loading the project configuration file fails
164    pub fn load_from_file(&mut self, file_path: &Path) -> Result<()> {
165        if !file_path.exists() {
166            return Err(eyre!("Configuration file not found: {}", file_path.display()));
167        }
168
169        self.config = Some(ProjectConfig::load(file_path)?);
170        self.config_dir = file_path.to_path_buf();
171
172        Ok(())
173    }
174
175    /// Validate required variables
176    ///
177    /// # Errors
178    ///
179    /// This function will return an error if:
180    /// - No project configuration is loaded
181    /// - Regex compilation fails for pattern validation
182    pub fn validate(&self, manager: &EnvVarManager) -> Result<ValidationReport> {
183        let config = self
184            .config
185            .as_ref()
186            .ok_or_else(|| color_eyre::eyre::eyre!("No project configuration loaded"))?;
187
188        let mut report = ValidationReport::default();
189
190        // Check required variables
191        for required in &config.required {
192            match manager.get(&required.name) {
193                Some(var) => {
194                    // Validate pattern if specified
195                    if let Some(pattern) = &required.pattern {
196                        let re = Regex::new(pattern)?;
197                        if !re.is_match(&var.value) {
198                            report.errors.push(ValidationError {
199                                var_name: required.name.clone(),
200                                error_type: ErrorType::PatternMismatch,
201                                message: format!("Value does not match pattern: {pattern}"),
202                            });
203                        }
204                    }
205                    report.found.push(required.name.clone());
206                }
207                None => {
208                    report.missing.push(MissingVar {
209                        name: required.name.clone(),
210                        description: required.description.clone(),
211                        example: required.example.clone(),
212                    });
213                }
214            }
215        }
216
217        // Check validation rules
218        if config.validation.strict_names {
219            for var in manager.list() {
220                if !is_valid_var_name(&var.name) {
221                    report.warnings.push(ValidationWarning {
222                        var_name: var.name.clone(),
223                        message: "Invalid variable name format".to_string(),
224                    });
225                }
226            }
227        }
228
229        report.success = report.errors.is_empty() && report.missing.is_empty();
230        Ok(report)
231    }
232
233    /// Run a project script
234    ///
235    /// # Errors
236    ///
237    /// This function will return an error if:
238    /// - No project configuration is loaded
239    /// - The specified script is not found in the configuration
240    /// - Setting environment variables fails
241    /// - The script execution fails
242    pub fn run_script(&self, script_name: &str, manager: &mut EnvVarManager) -> Result<()> {
243        let config = self
244            .config
245            .as_ref()
246            .ok_or_else(|| color_eyre::eyre::eyre!("No project configuration loaded"))?;
247
248        let script = config
249            .scripts
250            .get(script_name)
251            .ok_or_else(|| color_eyre::eyre::eyre!("Script '{}' not found", script_name))?;
252
253        // Apply script-specific environment variables
254        for (name, value) in &script.env {
255            manager.set(name, value, false)?;
256        }
257
258        // Execute the script
259        #[cfg(unix)]
260        {
261            std::process::Command::new("sh").arg("-c").arg(&script.run).status()?;
262        }
263
264        #[cfg(windows)]
265        {
266            std::process::Command::new("cmd").arg("/C").arg(&script.run).status()?;
267        }
268
269        Ok(())
270    }
271
272    fn load_env_file(path: &Path, manager: &mut EnvVarManager) -> Result<()> {
273        let content = fs::read_to_string(path)?;
274
275        for line in content.lines() {
276            let line = line.trim();
277            if line.is_empty() || line.starts_with('#') {
278                continue;
279            }
280
281            if let Some((key, value)) = line.split_once('=') {
282                let key = key.trim();
283                let value = value.trim().trim_matches('"').trim_matches('\'');
284                manager.set(key, value, true)?;
285            }
286        }
287
288        Ok(())
289    }
290}
291
292#[derive(Debug, Default)]
293pub struct ValidationReport {
294    pub success: bool,
295    pub missing: Vec<MissingVar>,
296    pub found: Vec<String>,
297    pub errors: Vec<ValidationError>,
298    pub warnings: Vec<ValidationWarning>,
299}
300
301#[derive(Debug)]
302pub struct MissingVar {
303    pub name: String,
304    pub description: Option<String>,
305    pub example: Option<String>,
306}
307
308#[derive(Debug)]
309pub struct ValidationError {
310    pub var_name: String,
311    pub error_type: ErrorType,
312    pub message: String,
313}
314
315#[derive(Debug)]
316pub enum ErrorType {
317    PatternMismatch,
318    InvalidValue,
319}
320
321#[derive(Debug)]
322pub struct ValidationWarning {
323    pub var_name: String,
324    pub message: String,
325}
326
327fn is_valid_var_name(name: &str) -> bool {
328    // Unix/Windows compatible variable name
329    let re = Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap();
330    re.is_match(name)
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use crate::project_config::{RequiredVar, Script};
337    use ahash::AHashMap as HashMap;
338    use tempfile::TempDir;
339
340    fn create_test_project_manager() -> (ProjectManager, TempDir) {
341        let temp_dir = TempDir::new().unwrap();
342        let current_dir = temp_dir.path().to_path_buf();
343
344        let manager = ProjectManager {
345            config_dir: current_dir.join(".envx"),
346            config: None,
347            current_dir: current_dir.clone(),
348        };
349
350        (manager, temp_dir)
351    }
352
353    fn create_test_env_manager() -> EnvVarManager {
354        let mut manager = EnvVarManager::new();
355        manager.set("EXISTING_VAR", "existing_value", false).unwrap();
356        manager
357    }
358
359    fn create_test_profile_manager() -> ProfileManager {
360        ProfileManager::new().unwrap()
361    }
362
363    fn create_test_config() -> ProjectConfig {
364        let mut config = ProjectConfig::new(Some("test-project".to_string()));
365
366        // Add required variables
367        config.required.push(RequiredVar {
368            name: "DATABASE_URL".to_string(),
369            description: Some("Database connection string".to_string()),
370            pattern: Some(r"^(postgresql|mysql)://.*".to_string()),
371            example: Some("postgresql://localhost/mydb".to_string()),
372        });
373
374        config.required.push(RequiredVar {
375            name: "API_KEY".to_string(),
376            description: Some("API authentication key".to_string()),
377            pattern: None,
378            example: None,
379        });
380
381        // Add defaults
382        config
383            .defaults
384            .insert("NODE_ENV".to_string(), "development".to_string());
385        config.defaults.insert("PORT".to_string(), "3000".to_string());
386
387        // Add scripts
388        let mut script_env = HashMap::new();
389        script_env.insert("NODE_ENV".to_string(), "test".to_string());
390
391        config.scripts.insert(
392            "test".to_string(),
393            Script {
394                description: Some("Run tests".to_string()),
395                run: "echo Running tests".to_string(),
396                env: script_env,
397            },
398        );
399
400        config
401    }
402
403    #[test]
404    fn test_project_manager_new() {
405        let result = ProjectManager::new();
406        assert!(result.is_ok());
407
408        let manager = result.unwrap();
409        assert_eq!(manager.config_dir, PathBuf::from(".envx"));
410        assert!(manager.config.is_none());
411    }
412
413    #[test]
414    fn test_init_creates_structure() {
415        let (manager, temp_dir) = create_test_project_manager();
416
417        let result = manager.init(Some("test-project".to_string()));
418        assert!(result.is_ok());
419
420        // Verify .envx directory exists
421        let envx_dir = temp_dir.path().join(".envx");
422        assert!(envx_dir.exists());
423        assert!(envx_dir.is_dir());
424
425        // Verify config.yaml exists
426        let config_path = envx_dir.join("config.yaml");
427        assert!(config_path.exists());
428
429        // Verify .gitignore exists
430        let gitignore_path = envx_dir.join(".gitignore");
431        assert!(gitignore_path.exists());
432
433        // Verify gitignore content
434        let gitignore_content = fs::read_to_string(gitignore_path).unwrap();
435        assert!(gitignore_content.contains("local/"));
436        assert!(gitignore_content.contains("*.local.yaml"));
437    }
438
439    #[test]
440    fn test_init_creates_valid_config() {
441        let (manager, temp_dir) = create_test_project_manager();
442
443        manager.init(Some("my-app".to_string())).unwrap();
444
445        let config_path = temp_dir.path().join(".envx").join("config.yaml");
446        let config = ProjectConfig::load(&config_path).unwrap();
447
448        assert_eq!(config.name, Some("my-app".to_string()));
449        assert!(config.required.is_empty());
450        assert!(config.defaults.is_empty());
451        assert_eq!(config.auto_load, vec![".env".to_string()]);
452    }
453
454    #[test]
455    fn test_find_and_load_in_current_dir() {
456        let (mut manager, temp_dir) = create_test_project_manager();
457
458        // Create config in current dir
459        manager.init(Some("test".to_string())).unwrap();
460
461        let result = manager.find_and_load();
462        assert!(result.is_ok());
463
464        let found_path = result.unwrap();
465        assert!(found_path.is_some());
466        assert_eq!(found_path.unwrap(), temp_dir.path());
467        assert!(manager.config.is_some());
468    }
469
470    #[test]
471    fn test_find_and_load_in_parent_dir() {
472        let temp_dir = TempDir::new().unwrap();
473        let parent_dir = temp_dir.path();
474        let child_dir = parent_dir.join("subdir");
475        fs::create_dir(&child_dir).unwrap();
476
477        // Create config in parent
478        let parent_manager = ProjectManager {
479            config_dir: parent_dir.join(".envx"),
480            config: None,
481            current_dir: parent_dir.to_path_buf(),
482        };
483        parent_manager.init(Some("parent-project".to_string())).unwrap();
484
485        // Try to find from child
486        let mut child_manager = ProjectManager {
487            config_dir: PathBuf::from(".envx"),
488            config: None,
489            current_dir: child_dir,
490        };
491
492        let result = child_manager.find_and_load();
493        assert!(result.is_ok());
494
495        let found_path = result.unwrap();
496        assert!(found_path.is_some());
497        assert_eq!(found_path.unwrap(), parent_dir);
498        assert!(child_manager.config.is_some());
499    }
500
501    #[test]
502    fn test_find_and_load_not_found() {
503        let (mut manager, _temp) = create_test_project_manager();
504
505        let result = manager.find_and_load();
506        assert!(result.is_ok());
507        assert!(result.unwrap().is_none());
508        assert!(manager.config.is_none());
509    }
510
511    #[test]
512    fn test_apply_loads_env_files() {
513        let (mut manager, temp_dir) = create_test_project_manager();
514        let mut env_manager = create_test_env_manager();
515        let mut profile_manager = create_test_profile_manager();
516
517        // Create .env file
518        let env_content = "TEST_VAR=test_value\nANOTHER_VAR=another_value";
519        fs::write(temp_dir.path().join(".env"), env_content).unwrap();
520
521        // Create config with auto_load
522        let mut config = create_test_config();
523        config.auto_load = vec![".env".to_string()];
524        manager.config = Some(config);
525
526        let result = manager.apply(&mut env_manager, &mut profile_manager);
527        assert!(result.is_ok());
528
529        assert_eq!(env_manager.get("TEST_VAR").unwrap().value, "test_value");
530        assert_eq!(env_manager.get("ANOTHER_VAR").unwrap().value, "another_value");
531    }
532
533    #[test]
534    fn test_apply_sets_defaults() {
535        let (mut manager, _temp) = create_test_project_manager();
536        let mut env_manager = create_test_env_manager();
537        let mut profile_manager = create_test_profile_manager();
538
539        manager.config = Some(create_test_config());
540
541        let result = manager.apply(&mut env_manager, &mut profile_manager);
542        assert!(result.is_ok());
543
544        assert_eq!(env_manager.get("NODE_ENV").unwrap().value, "development");
545        assert_eq!(env_manager.get("PORT").unwrap().value, "3000");
546    }
547
548    #[test]
549    fn test_apply_doesnt_override_existing() {
550        let (mut manager, _temp) = create_test_project_manager();
551        let mut env_manager = create_test_env_manager();
552        let mut profile_manager = create_test_profile_manager();
553
554        // Set a variable that's also in defaults
555        env_manager.set("NODE_ENV", "production", false).unwrap();
556
557        manager.config = Some(create_test_config());
558
559        manager.apply(&mut env_manager, &mut profile_manager).unwrap();
560
561        // Should not override existing value
562        assert_eq!(env_manager.get("NODE_ENV").unwrap().value, "production");
563    }
564
565    #[test]
566    fn test_apply_no_config_error() {
567        let (manager, _temp) = create_test_project_manager();
568        let mut env_manager = create_test_env_manager();
569        let mut profile_manager = create_test_profile_manager();
570
571        let result = manager.apply(&mut env_manager, &mut profile_manager);
572        assert!(result.is_err());
573        assert!(
574            result
575                .unwrap_err()
576                .to_string()
577                .contains("No project configuration loaded")
578        );
579    }
580
581    #[test]
582    fn test_validate_all_present_and_valid() {
583        let (mut manager, _temp) = create_test_project_manager();
584        let mut env_manager = create_test_env_manager();
585
586        // Set required variables
587        env_manager
588            .set("DATABASE_URL", "postgresql://localhost/mydb", false)
589            .unwrap();
590        env_manager.set("API_KEY", "secret-key", false).unwrap();
591
592        manager.config = Some(create_test_config());
593
594        let report = manager.validate(&env_manager).unwrap();
595        assert!(report.success);
596        assert!(report.missing.is_empty());
597        assert!(report.errors.is_empty());
598        assert_eq!(report.found.len(), 2);
599    }
600
601    #[test]
602    fn test_validate_missing_variables() {
603        let (mut manager, _temp) = create_test_project_manager();
604        let env_manager = create_test_env_manager();
605
606        manager.config = Some(create_test_config());
607
608        let report = manager.validate(&env_manager).unwrap();
609        assert!(!report.success);
610        assert_eq!(report.missing.len(), 2);
611
612        let missing_names: Vec<&str> = report.missing.iter().map(|m| m.name.as_str()).collect();
613        assert!(missing_names.contains(&"DATABASE_URL"));
614        assert!(missing_names.contains(&"API_KEY"));
615    }
616
617    #[test]
618    fn test_validate_pattern_mismatch() {
619        let (mut manager, _temp) = create_test_project_manager();
620        let mut env_manager = create_test_env_manager();
621
622        // Set with invalid pattern
623        env_manager.set("DATABASE_URL", "invalid-url", false).unwrap();
624        env_manager.set("API_KEY", "valid-key", false).unwrap();
625
626        manager.config = Some(create_test_config());
627
628        let report = manager.validate(&env_manager).unwrap();
629        assert!(!report.success);
630        assert_eq!(report.errors.len(), 1);
631        assert_eq!(report.errors[0].var_name, "DATABASE_URL");
632        assert!(matches!(report.errors[0].error_type, ErrorType::PatternMismatch));
633    }
634
635    #[test]
636    fn test_validate_strict_names() {
637        let (mut manager, _temp) = create_test_project_manager();
638        let mut env_manager = create_test_env_manager();
639
640        // Set variable with invalid name
641        env_manager.vars.insert(
642            "invalid-name".to_string(),
643            crate::EnvVar {
644                name: "invalid-name".to_string(),
645                value: "value".to_string(),
646                source: crate::EnvVarSource::User,
647                modified: chrono::Utc::now(),
648                original_value: None,
649            },
650        );
651
652        let mut config = create_test_config();
653        config.validation.strict_names = true;
654        manager.config = Some(config);
655
656        let report = manager.validate(&env_manager).unwrap();
657        assert!(!report.warnings.is_empty());
658        assert!(report.warnings.iter().any(|w| w.var_name == "invalid-name"));
659    }
660
661    #[test]
662    fn test_run_script_success() {
663        let (mut manager, _temp) = create_test_project_manager();
664        let mut env_manager = create_test_env_manager();
665
666        manager.config = Some(create_test_config());
667
668        let result = manager.run_script("test", &mut env_manager);
669        assert!(result.is_ok());
670
671        // Verify script environment was applied
672        assert_eq!(env_manager.get("NODE_ENV").unwrap().value, "test");
673    }
674
675    #[test]
676    fn test_run_script_not_found() {
677        let (mut manager, _temp) = create_test_project_manager();
678        let mut env_manager = create_test_env_manager();
679
680        manager.config = Some(create_test_config());
681
682        let result = manager.run_script("nonexistent", &mut env_manager);
683        assert!(result.is_err());
684        assert!(
685            result
686                .unwrap_err()
687                .to_string()
688                .contains("Script 'nonexistent' not found")
689        );
690    }
691
692    #[test]
693    fn test_run_script_no_config() {
694        let (manager, _temp) = create_test_project_manager();
695        let mut env_manager = create_test_env_manager();
696
697        let result = manager.run_script("test", &mut env_manager);
698        assert!(result.is_err());
699        assert!(
700            result
701                .unwrap_err()
702                .to_string()
703                .contains("No project configuration loaded")
704        );
705    }
706
707    #[test]
708    fn test_load_env_file_basic() {
709        let temp_dir = TempDir::new().unwrap();
710        let env_path = temp_dir.path().join(".env");
711        let mut env_manager = create_test_env_manager();
712
713        let content = r#"
714# Comment line
715VAR1=value1
716VAR2="quoted value"
717VAR3='single quoted'
718EMPTY_LINE_ABOVE=yes
719
720# Another comment
721VAR_WITH_SPACES = spaced value
722"#;
723        fs::write(&env_path, content).unwrap();
724
725        ProjectManager::load_env_file(&env_path, &mut env_manager).unwrap();
726
727        assert_eq!(env_manager.get("VAR1").unwrap().value, "value1");
728        assert_eq!(env_manager.get("VAR2").unwrap().value, "quoted value");
729        assert_eq!(env_manager.get("VAR3").unwrap().value, "single quoted");
730        assert_eq!(env_manager.get("EMPTY_LINE_ABOVE").unwrap().value, "yes");
731        assert_eq!(env_manager.get("VAR_WITH_SPACES").unwrap().value, "spaced value");
732    }
733
734    #[test]
735    fn test_load_env_file_edge_cases() {
736        let temp_dir = TempDir::new().unwrap();
737        let env_path = temp_dir.path().join(".env");
738        let mut env_manager = create_test_env_manager();
739
740        let content = r"
741# Edge cases
742EMPTY_VALUE=
743EQUALS_IN_VALUE=key=value=more
744URL=https://example.com/path?query=value
745MULTILINE_ATTEMPT=line1\nline2
746SPECIAL_CHARS=!@#$%^&*()
747";
748        fs::write(&env_path, content).unwrap();
749
750        ProjectManager::load_env_file(&env_path, &mut env_manager).unwrap();
751
752        assert_eq!(env_manager.get("EMPTY_VALUE").unwrap().value, "");
753        assert_eq!(env_manager.get("EQUALS_IN_VALUE").unwrap().value, "key=value=more");
754        assert_eq!(
755            env_manager.get("URL").unwrap().value,
756            "https://example.com/path?query=value"
757        );
758        assert_eq!(env_manager.get("MULTILINE_ATTEMPT").unwrap().value, "line1\\nline2");
759        assert_eq!(env_manager.get("SPECIAL_CHARS").unwrap().value, "!@#$%^&*()");
760    }
761
762    #[test]
763    fn test_load_env_file_not_found() {
764        let mut env_manager = create_test_env_manager();
765        let result = ProjectManager::load_env_file(Path::new("/nonexistent/.env"), &mut env_manager);
766        assert!(result.is_err());
767    }
768
769    #[test]
770    fn test_is_valid_var_name() {
771        // Valid names
772        assert!(is_valid_var_name("VAR"));
773        assert!(is_valid_var_name("VAR_NAME"));
774        assert!(is_valid_var_name("_PRIVATE"));
775        assert!(is_valid_var_name("var123"));
776        assert!(is_valid_var_name("V"));
777        assert!(is_valid_var_name("VERY_LONG_VARIABLE_NAME_WITH_UNDERSCORES"));
778
779        // Invalid names
780        assert!(!is_valid_var_name("123VAR")); // Starts with number
781        assert!(!is_valid_var_name("VAR-NAME")); // Contains dash
782        assert!(!is_valid_var_name("VAR NAME")); // Contains space
783        assert!(!is_valid_var_name("VAR.NAME")); // Contains dot
784        assert!(!is_valid_var_name("")); // Empty
785        assert!(!is_valid_var_name("VAR$")); // Contains special char
786        assert!(!is_valid_var_name("@VAR")); // Starts with special char
787    }
788
789    #[test]
790    fn test_validation_report_success_calculation() {
791        let mut report = ValidationReport::default();
792        // Default should be false because Default trait initializes bool as false
793        assert!(!report.success);
794
795        // Manually set success based on empty errors and missing
796        report.success = report.errors.is_empty() && report.missing.is_empty();
797        assert!(report.success); // Now it should be true since both are empty
798
799        // Add missing variable
800        report.missing.push(MissingVar {
801            name: "VAR".to_string(),
802            description: None,
803            example: None,
804        });
805        report.success = report.errors.is_empty() && report.missing.is_empty();
806        assert!(!report.success);
807
808        // Clear missing
809        report.missing.clear();
810        report.success = report.errors.is_empty() && report.missing.is_empty();
811        assert!(report.success);
812
813        // Add error
814        report.errors.push(ValidationError {
815            var_name: "VAR".to_string(),
816            error_type: ErrorType::PatternMismatch,
817            message: "error".to_string(),
818        });
819        report.success = report.errors.is_empty() && report.missing.is_empty();
820        assert!(!report.success);
821
822        // Clear everything
823        report.errors.clear();
824        report.missing.clear();
825        report.success = report.errors.is_empty() && report.missing.is_empty();
826        assert!(report.success);
827    }
828}