1use crate::error::Result;
6use crate::models::{Project, ProjectDependency, RuleType, Workspace, WorkspaceRule};
7use std::collections::{HashMap, HashSet};
8
9#[derive(Debug, Clone)]
11pub struct RulesValidator {
12 workspace: Workspace,
14}
15
16#[derive(Debug, Clone)]
18pub struct ValidationResult {
19 pub passed: bool,
21
22 pub violations: Vec<RuleViolation>,
24
25 pub warnings: Vec<String>,
27}
28
29#[derive(Debug, Clone)]
31pub struct RuleViolation {
32 pub rule_name: String,
34
35 pub rule_type: RuleType,
37
38 pub description: String,
40
41 pub affected_projects: Vec<String>,
43
44 pub severity: ViolationSeverity,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
50pub enum ViolationSeverity {
51 Warning,
53
54 Error,
56
57 Critical,
59}
60
61impl RulesValidator {
62 pub fn new(workspace: Workspace) -> Self {
64 Self { workspace }
65 }
66
67 pub fn validate_all(&self) -> Result<ValidationResult> {
69 let mut violations = Vec::new();
70 let mut warnings = Vec::new();
71
72 for rule in &self.workspace.config.rules {
73 if !rule.enabled {
74 continue;
75 }
76
77 match rule.rule_type {
78 RuleType::DependencyConstraint => {
79 if let Err(e) = self.validate_dependency_constraints(rule, &mut violations) {
80 warnings.push(format!("Error validating dependency constraints: {}", e));
81 }
82 }
83 RuleType::NamingConvention => {
84 if let Err(e) = self.validate_naming_conventions(rule, &mut violations) {
85 warnings.push(format!("Error validating naming conventions: {}", e));
86 }
87 }
88 RuleType::ArchitecturalBoundary => {
89 if let Err(e) = self.validate_architectural_boundaries(rule, &mut violations) {
90 warnings.push(format!("Error validating architectural boundaries: {}", e));
91 }
92 }
93 }
94 }
95
96 let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
97
98 Ok(ValidationResult {
99 passed,
100 violations,
101 warnings,
102 })
103 }
104
105 fn validate_dependency_constraints(
107 &self,
108 rule: &WorkspaceRule,
109 violations: &mut Vec<RuleViolation>,
110 ) -> Result<()> {
111 if rule.name == "no-circular-deps" {
113 let circular_deps = self.find_circular_dependencies();
114 for (projects, cycle) in circular_deps {
115 violations.push(RuleViolation {
116 rule_name: rule.name.clone(),
117 rule_type: rule.rule_type,
118 description: format!("Circular dependency detected: {}", cycle),
119 affected_projects: projects,
120 severity: ViolationSeverity::Error,
121 });
122 }
123 }
124
125 Ok(())
126 }
127
128 fn validate_naming_conventions(
130 &self,
131 rule: &WorkspaceRule,
132 violations: &mut Vec<RuleViolation>,
133 ) -> Result<()> {
134 if rule.name == "naming-convention" {
136 for project in &self.workspace.projects {
137 if !self.is_valid_project_name(&project.name) {
139 violations.push(RuleViolation {
140 rule_name: rule.name.clone(),
141 rule_type: rule.rule_type,
142 description: format!(
143 "Project name '{}' does not follow naming convention (should be lowercase with hyphens)",
144 project.name
145 ),
146 affected_projects: vec![project.name.clone()],
147 severity: ViolationSeverity::Warning,
148 });
149 }
150 }
151 }
152
153 Ok(())
154 }
155
156 fn validate_architectural_boundaries(
158 &self,
159 rule: &WorkspaceRule,
160 violations: &mut Vec<RuleViolation>,
161 ) -> Result<()> {
162 if rule.name == "no-cross-layer-deps" {
164 let cross_layer_deps = self.find_cross_layer_dependencies();
165 for (from, to) in cross_layer_deps {
166 violations.push(RuleViolation {
167 rule_name: rule.name.clone(),
168 rule_type: rule.rule_type,
169 description: format!(
170 "Cross-layer dependency detected: {} depends on {}",
171 from, to
172 ),
173 affected_projects: vec![from, to],
174 severity: ViolationSeverity::Warning,
175 });
176 }
177 }
178
179 Ok(())
180 }
181
182 fn find_circular_dependencies(&self) -> Vec<(Vec<String>, String)> {
184 let mut circular_deps = Vec::new();
185 let mut visited = HashSet::new();
186 let mut rec_stack = HashSet::new();
187
188 for project in &self.workspace.projects {
189 if !visited.contains(&project.name) {
190 if let Some(cycle) = self.find_cycle(&project.name, &mut visited, &mut rec_stack) {
191 circular_deps.push((cycle.clone(), cycle.join(" -> ")));
192 }
193 }
194 }
195
196 circular_deps
197 }
198
199 fn find_cycle(
201 &self,
202 project: &str,
203 visited: &mut HashSet<String>,
204 rec_stack: &mut HashSet<String>,
205 ) -> Option<Vec<String>> {
206 visited.insert(project.to_string());
207 rec_stack.insert(project.to_string());
208
209 let deps: Vec<String> = self
211 .workspace
212 .dependencies
213 .iter()
214 .filter(|d| d.from == project)
215 .map(|d| d.to.clone())
216 .collect();
217
218 for dep in deps {
219 if !visited.contains(&dep) {
220 if let Some(mut cycle) = self.find_cycle(&dep, visited, rec_stack) {
221 cycle.insert(0, project.to_string());
222 return Some(cycle);
223 }
224 } else if rec_stack.contains(&dep) {
225 return Some(vec![project.to_string(), dep]);
226 }
227 }
228
229 rec_stack.remove(project);
230 None
231 }
232
233 fn is_valid_project_name(&self, name: &str) -> bool {
235 name.chars().all(|c| c.is_ascii_lowercase() || c == '-' || c.is_ascii_digit())
237 && !name.starts_with('-')
238 && !name.ends_with('-')
239 }
240
241 fn find_cross_layer_dependencies(&self) -> Vec<(String, String)> {
243 let mut cross_layer_deps = Vec::new();
244
245 let layers = self.determine_project_layers();
247
248 for dep in &self.workspace.dependencies {
249 let from_layer = layers.get(&dep.from).copied().unwrap_or(0);
250 let to_layer = layers.get(&dep.to).copied().unwrap_or(0);
251
252 if from_layer > to_layer {
254 cross_layer_deps.push((dep.from.clone(), dep.to.clone()));
255 }
256 }
257
258 cross_layer_deps
259 }
260
261 fn determine_project_layers(&self) -> HashMap<String, u32> {
263 let mut layers = HashMap::new();
264
265 for project in &self.workspace.projects {
266 let layer = if project.name.starts_with("ricecoder-core") {
267 0
268 } else if project.name.starts_with("ricecoder-") {
269 1
270 } else {
271 2
272 };
273
274 layers.insert(project.name.clone(), layer);
275 }
276
277 layers
278 }
279
280 pub fn validate_project(&self, project: &Project) -> Result<ValidationResult> {
282 let mut violations = Vec::new();
283 let warnings = Vec::new();
284
285 for rule in &self.workspace.config.rules {
286 if !rule.enabled {
287 continue;
288 }
289
290 if rule.rule_type == RuleType::NamingConvention && !self.is_valid_project_name(&project.name) {
291 violations.push(RuleViolation {
292 rule_name: rule.name.clone(),
293 rule_type: rule.rule_type,
294 description: format!(
295 "Project name '{}' does not follow naming convention",
296 project.name
297 ),
298 affected_projects: vec![project.name.clone()],
299 severity: ViolationSeverity::Warning,
300 });
301 }
302 }
303
304 let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
305
306 Ok(ValidationResult {
307 passed,
308 violations,
309 warnings,
310 })
311 }
312
313 pub fn validate_dependency(&self, dep: &ProjectDependency) -> Result<ValidationResult> {
315 let mut violations = Vec::new();
316 let warnings = Vec::new();
317
318 for rule in &self.workspace.config.rules {
319 if !rule.enabled {
320 continue;
321 }
322
323 if rule.rule_type == RuleType::DependencyConstraint {
324 if self.would_create_cycle(&dep.from, &dep.to) {
326 violations.push(RuleViolation {
327 rule_name: rule.name.clone(),
328 rule_type: rule.rule_type,
329 description: format!(
330 "Dependency would create a cycle: {} -> {}",
331 dep.from, dep.to
332 ),
333 affected_projects: vec![dep.from.clone(), dep.to.clone()],
334 severity: ViolationSeverity::Error,
335 });
336 }
337 }
338 }
339
340 let passed = violations.iter().all(|v| v.severity == ViolationSeverity::Warning);
341
342 Ok(ValidationResult {
343 passed,
344 violations,
345 warnings,
346 })
347 }
348
349 fn would_create_cycle(&self, from: &str, to: &str) -> bool {
351 self.has_path(to, from)
353 }
354
355 fn has_path(&self, from: &str, to: &str) -> bool {
357 if from == to {
358 return true;
359 }
360
361 let mut visited = HashSet::new();
362 self.has_path_recursive(from, to, &mut visited)
363 }
364
365 fn has_path_recursive(&self, from: &str, to: &str, visited: &mut HashSet<String>) -> bool {
367 if from == to {
368 return true;
369 }
370
371 if visited.contains(from) {
372 return false;
373 }
374
375 visited.insert(from.to_string());
376
377 for dep in &self.workspace.dependencies {
378 if dep.from == from && self.has_path_recursive(&dep.to, to, visited) {
379 return true;
380 }
381 }
382
383 false
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::models::{ProjectStatus, WorkspaceConfig, WorkspaceMetrics};
391
392 fn create_test_workspace() -> Workspace {
393 Workspace {
394 root: std::path::PathBuf::from("/workspace"),
395 projects: vec![
396 Project {
397 path: std::path::PathBuf::from("/workspace/project-a"),
398 name: "project-a".to_string(),
399 project_type: "rust".to_string(),
400 version: "0.1.0".to_string(),
401 status: ProjectStatus::Healthy,
402 },
403 Project {
404 path: std::path::PathBuf::from("/workspace/project-b"),
405 name: "project-b".to_string(),
406 project_type: "rust".to_string(),
407 version: "0.1.0".to_string(),
408 status: ProjectStatus::Healthy,
409 },
410 ],
411 dependencies: vec![],
412 config: WorkspaceConfig::default(),
413 metrics: WorkspaceMetrics::default(),
414 }
415 }
416
417 #[test]
418 fn test_rules_validator_creation() {
419 let workspace = create_test_workspace();
420 let validator = RulesValidator::new(workspace);
421 assert_eq!(validator.workspace.projects.len(), 2);
422 }
423
424 #[test]
425 fn test_is_valid_project_name() {
426 let workspace = create_test_workspace();
427 let validator = RulesValidator::new(workspace);
428
429 assert!(validator.is_valid_project_name("project-a"));
430 assert!(validator.is_valid_project_name("my-project-123"));
431 assert!(!validator.is_valid_project_name("Project-A"));
432 assert!(!validator.is_valid_project_name("-project"));
433 assert!(!validator.is_valid_project_name("project-"));
434 }
435
436 #[test]
437 fn test_validate_naming_conventions() {
438 let mut workspace = create_test_workspace();
439 workspace.projects.push(Project {
440 path: std::path::PathBuf::from("/workspace/InvalidProject"),
441 name: "InvalidProject".to_string(),
442 project_type: "rust".to_string(),
443 version: "0.1.0".to_string(),
444 status: ProjectStatus::Healthy,
445 });
446
447 workspace.config.rules.push(WorkspaceRule {
449 name: "naming-convention".to_string(),
450 rule_type: RuleType::NamingConvention,
451 enabled: true,
452 });
453
454 let validator = RulesValidator::new(workspace);
455 let result = validator.validate_all().unwrap();
456
457 assert!(!result.violations.is_empty());
458 }
459
460 #[test]
461 fn test_find_circular_dependencies() {
462 let mut workspace = create_test_workspace();
463 workspace.dependencies = vec![
464 ProjectDependency {
465 from: "project-a".to_string(),
466 to: "project-b".to_string(),
467 dependency_type: crate::models::DependencyType::Direct,
468 version_constraint: "^0.1.0".to_string(),
469 },
470 ProjectDependency {
471 from: "project-b".to_string(),
472 to: "project-a".to_string(),
473 dependency_type: crate::models::DependencyType::Direct,
474 version_constraint: "^0.1.0".to_string(),
475 },
476 ];
477
478 let validator = RulesValidator::new(workspace);
479 let circular_deps = validator.find_circular_dependencies();
480
481 assert!(!circular_deps.is_empty());
482 }
483
484 #[test]
485 fn test_would_create_cycle() {
486 let mut workspace = create_test_workspace();
487 workspace.dependencies = vec![ProjectDependency {
488 from: "project-a".to_string(),
489 to: "project-b".to_string(),
490 dependency_type: crate::models::DependencyType::Direct,
491 version_constraint: "^0.1.0".to_string(),
492 }];
493
494 let validator = RulesValidator::new(workspace);
495
496 assert!(validator.would_create_cycle("project-b", "project-a"));
498
499 assert!(!validator.would_create_cycle("project-b", "project-c"));
501 }
502
503 #[test]
504 fn test_validate_project() {
505 let workspace = create_test_workspace();
506 let validator = RulesValidator::new(workspace);
507
508 let project = Project {
509 path: std::path::PathBuf::from("/workspace/project-a"),
510 name: "project-a".to_string(),
511 project_type: "rust".to_string(),
512 version: "0.1.0".to_string(),
513 status: ProjectStatus::Healthy,
514 };
515
516 let result = validator.validate_project(&project).unwrap();
517 assert!(result.passed);
518 }
519
520 #[test]
521 fn test_validate_dependency() {
522 let workspace = create_test_workspace();
523 let validator = RulesValidator::new(workspace);
524
525 let dep = ProjectDependency {
526 from: "project-a".to_string(),
527 to: "project-b".to_string(),
528 dependency_type: crate::models::DependencyType::Direct,
529 version_constraint: "^0.1.0".to_string(),
530 };
531
532 let result = validator.validate_dependency(&dep).unwrap();
533 assert!(result.passed);
534 }
535
536 #[test]
537 fn test_has_path() {
538 let mut workspace = create_test_workspace();
539 workspace.dependencies = vec![
540 ProjectDependency {
541 from: "project-a".to_string(),
542 to: "project-b".to_string(),
543 dependency_type: crate::models::DependencyType::Direct,
544 version_constraint: "^0.1.0".to_string(),
545 },
546 ];
547
548 let validator = RulesValidator::new(workspace);
549
550 assert!(validator.has_path("project-a", "project-b"));
551 assert!(!validator.has_path("project-b", "project-a"));
552 }
553
554 #[test]
555 fn test_determine_project_layers() {
556 let mut workspace = create_test_workspace();
557 workspace.projects.push(Project {
558 path: std::path::PathBuf::from("/workspace/ricecoder-core"),
559 name: "ricecoder-core".to_string(),
560 project_type: "rust".to_string(),
561 version: "0.1.0".to_string(),
562 status: ProjectStatus::Healthy,
563 });
564
565 let validator = RulesValidator::new(workspace);
566 let layers = validator.determine_project_layers();
567
568 assert_eq!(layers.get("ricecoder-core"), Some(&0));
569 assert_eq!(layers.get("project-a"), Some(&2));
570 }
571}