raz_config/
validation.rs

1use crate::{
2    GlobalConfig, WorkspaceConfig,
3    error::{ConfigError, Result},
4    schema::{ConfigSchema, ConfigVersion},
5};
6
7pub struct ConfigValidator {
8    schema: ConfigSchema,
9}
10
11impl ConfigValidator {
12    pub fn new(version: ConfigVersion) -> Self {
13        Self {
14            schema: ConfigSchema::for_version(version),
15        }
16    }
17
18    pub fn validate_global(&self, config: &GlobalConfig) -> Result<ValidationReport> {
19        let mut report = ValidationReport::new();
20
21        if config.raz.version != self.schema.version {
22            report.add_error(format!(
23                "Config version mismatch: expected {}, found {}",
24                self.schema.version, config.raz.version
25            ));
26        }
27
28        if config.raz.providers.is_empty() {
29            report.add_error("No providers enabled".to_string());
30        }
31
32        for provider in &config.raz.providers {
33            if !Self::is_valid_provider(provider) {
34                report.add_warning(format!("Unknown provider: {provider}"));
35            }
36        }
37
38        if let Some(cache_ttl) = config.raz.cache_ttl {
39            if cache_ttl == 0 {
40                report.add_error("Cache TTL must be greater than 0".to_string());
41            } else if cache_ttl > 86400 {
42                report.add_warning("Cache TTL is very high (> 24 hours)".to_string());
43            }
44        }
45
46        if let Some(jobs) = config.raz.max_concurrent_jobs {
47            if jobs == 0 {
48                report.add_error("Max concurrent jobs must be greater than 0".to_string());
49            } else if jobs > 32 {
50                report.add_warning(
51                    "Very high concurrent job limit may cause system instability".to_string(),
52                );
53            }
54        }
55
56        if let Some(cache_dir) = &config.raz.cache_dir {
57            if !cache_dir.is_absolute() {
58                report.add_error("Cache directory must be an absolute path".to_string());
59            }
60        }
61
62        self.validate_commands(&config.commands, &mut report);
63
64        if report.has_errors() {
65            Err(ConfigError::ValidationError(report.format_errors()))
66        } else {
67            Ok(report)
68        }
69    }
70
71    pub fn validate_workspace(&self, config: &WorkspaceConfig) -> Result<ValidationReport> {
72        let mut report = ValidationReport::new();
73
74        if let Some(raz_config) = &config.raz {
75            if raz_config.version != self.schema.version {
76                report.add_error(format!(
77                    "Config version mismatch: expected {}, found {}",
78                    self.schema.version, raz_config.version
79                ));
80            }
81        }
82
83        if let Some(extends) = &config.extends {
84            if !extends.exists() {
85                report.add_error(format!(
86                    "Extended config file not found: {}",
87                    extends.display()
88                ));
89            }
90        }
91
92        self.validate_commands(&config.commands, &mut report);
93
94        if report.has_errors() {
95            Err(ConfigError::ValidationError(report.format_errors()))
96        } else {
97            Ok(report)
98        }
99    }
100
101    fn validate_commands(
102        &self,
103        commands: &Option<Vec<crate::CommandConfig>>,
104        report: &mut ValidationReport,
105    ) {
106        if let Some(commands) = commands {
107            let mut seen_names = std::collections::HashSet::new();
108
109            for cmd in commands {
110                if !seen_names.insert(&cmd.name) {
111                    report.add_error(format!("Duplicate command name: {}", cmd.name));
112                }
113
114                if cmd.command.is_empty() {
115                    report.add_error(format!("Empty command for: {}", cmd.name));
116                }
117
118                if let Some(working_dir) = &cmd.working_dir {
119                    if !working_dir.is_absolute() {
120                        report.add_warning(format!(
121                            "Command '{}' uses relative working directory: {}",
122                            cmd.name,
123                            working_dir.display()
124                        ));
125                    }
126                }
127            }
128        }
129    }
130
131    fn is_valid_provider(provider: &str) -> bool {
132        matches!(
133            provider,
134            "cargo" | "rustc" | "leptos" | "dioxus" | "bevy" | "custom"
135        )
136    }
137}
138
139#[derive(Debug)]
140pub struct ValidationReport {
141    pub errors: Vec<String>,
142    pub warnings: Vec<String>,
143}
144
145impl ValidationReport {
146    pub fn new() -> Self {
147        Self {
148            errors: Vec::new(),
149            warnings: Vec::new(),
150        }
151    }
152
153    pub fn add_error(&mut self, error: String) {
154        self.errors.push(error);
155    }
156
157    pub fn add_warning(&mut self, warning: String) {
158        self.warnings.push(warning);
159    }
160
161    pub fn has_errors(&self) -> bool {
162        !self.errors.is_empty()
163    }
164
165    pub fn has_warnings(&self) -> bool {
166        !self.warnings.is_empty()
167    }
168
169    pub fn format_errors(&self) -> String {
170        self.errors.join("; ")
171    }
172
173    pub fn format_warnings(&self) -> String {
174        self.warnings.join("; ")
175    }
176}
177
178impl Default for ValidationReport {
179    fn default() -> Self {
180        Self::new()
181    }
182}