turbovault_tools/
validation_tools.rs

1//! Content validation tools
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use std::sync::Arc;
6use turbovault_core::{
7    CompositeValidator, ContentValidator, FrontmatterValidator, LinkValidator, Result, Severity,
8    ValidationReport, Validator,
9};
10use turbovault_vault::VaultManager;
11
12/// Validation tools context
13pub struct ValidationTools {
14    pub manager: Arc<VaultManager>,
15}
16
17/// Simplified validation issue for JSON serialization
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ValidationIssueInfo {
20    pub severity: String,
21    pub category: String,
22    pub message: String,
23    pub line: Option<usize>,
24    pub suggestion: Option<String>,
25}
26
27/// Simplified validation report for JSON serialization
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct ValidationReportInfo {
30    pub passed: bool,
31    pub total_issues: usize,
32    pub info_count: usize,
33    pub warning_count: usize,
34    pub error_count: usize,
35    pub critical_count: usize,
36    pub issues: Vec<ValidationIssueInfo>,
37}
38
39impl ValidationTools {
40    /// Create new validation tools
41    pub fn new(manager: Arc<VaultManager>) -> Self {
42        Self { manager }
43    }
44
45    /// Validate a single note
46    pub async fn validate_note(&self, path: &str) -> Result<ValidationReportInfo> {
47        let file_path = PathBuf::from(path);
48        let vault_file = self.manager.parse_file(&file_path).await?;
49
50        let validator = CompositeValidator::default_rules();
51        let report = validator.validate(&vault_file);
52
53        Ok(Self::convert_report(report))
54    }
55
56    /// Validate note with custom rules
57    pub async fn validate_note_with_rules(
58        &self,
59        path: &str,
60        require_frontmatter: bool,
61        required_fields: Vec<String>,
62        check_links: bool,
63        min_length: Option<usize>,
64    ) -> Result<ValidationReportInfo> {
65        let file_path = PathBuf::from(path);
66        let vault_file = self.manager.parse_file(&file_path).await?;
67
68        let mut validator = CompositeValidator::new();
69
70        // Add frontmatter validator if required
71        if require_frontmatter || !required_fields.is_empty() {
72            let mut fm_validator = FrontmatterValidator::new();
73            for field in required_fields {
74                fm_validator = fm_validator.require_field(field);
75            }
76            validator = validator.add_validator(Box::new(fm_validator));
77        }
78
79        // Add link validator if requested
80        if check_links {
81            validator = validator.add_validator(Box::new(LinkValidator::new()));
82        }
83
84        // Add content validator if min length specified
85        if let Some(min) = min_length {
86            let content_validator = ContentValidator::new().min_length(min);
87            validator = validator.add_validator(Box::new(content_validator));
88        }
89
90        let report = validator.validate(&vault_file);
91
92        Ok(Self::convert_report(report))
93    }
94
95    /// Validate entire vault (batch validation)
96    pub async fn validate_vault(&self) -> Result<ValidationReportInfo> {
97        let files = self.manager.scan_vault().await?;
98
99        let validator = CompositeValidator::default_rules();
100        let mut combined_report = ValidationReport::new();
101
102        for file_path in files {
103            if let Ok(vault_file) = self.manager.parse_file(&file_path).await {
104                let report = validator.validate(&vault_file);
105                combined_report.merge(report);
106            }
107        }
108
109        Ok(Self::convert_report(combined_report))
110    }
111
112    /// Validate vault with issue limit (for large vaults)
113    pub async fn validate_vault_quick(&self, max_issues: usize) -> Result<ValidationReportInfo> {
114        let files = self.manager.scan_vault().await?;
115
116        let validator = CompositeValidator::default_rules();
117        let mut combined_report = ValidationReport::new();
118
119        for file_path in files {
120            // Stop if we've hit the max issues
121            if combined_report.total_issues() >= max_issues {
122                break;
123            }
124
125            if let Ok(vault_file) = self.manager.parse_file(&file_path).await {
126                let report = validator.validate(&vault_file);
127                combined_report.merge(report);
128            }
129        }
130
131        Ok(Self::convert_report(combined_report))
132    }
133
134    /// Convert ValidationReport to serializable format
135    fn convert_report(report: ValidationReport) -> ValidationReportInfo {
136        ValidationReportInfo {
137            passed: report.passed,
138            total_issues: report.total_issues(),
139            info_count: report.summary.info_count,
140            warning_count: report.summary.warning_count,
141            error_count: report.summary.error_count,
142            critical_count: report.summary.critical_count,
143            issues: report
144                .issues
145                .into_iter()
146                .map(|issue| ValidationIssueInfo {
147                    severity: Self::severity_to_string(issue.severity),
148                    category: issue.category,
149                    message: issue.message,
150                    line: issue.line,
151                    suggestion: issue.suggestion,
152                })
153                .collect(),
154        }
155    }
156
157    /// Convert Severity enum to string
158    fn severity_to_string(severity: Severity) -> String {
159        match severity {
160            Severity::Info => "info".to_string(),
161            Severity::Warning => "warning".to_string(),
162            Severity::Error => "error".to_string(),
163            Severity::Critical => "critical".to_string(),
164        }
165    }
166}