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}