data_doctor_core/
report.rs

1//! # Report Module
2//!
3//! Provides structures for validation reports and issues.
4//! This module handles the collection, categorization, and formatting
5//! of validation results and data quality issues.
6
7use std::fmt;
8
9/// Severity level of a validation issue
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum Severity {
12    Info,
13    Warning,
14    Error,
15    Critical,
16}
17
18impl fmt::Display for Severity {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Severity::Info => write!(f, "INFO"),
22            Severity::Warning => write!(f, "WARNING"),
23            Severity::Error => write!(f, "ERROR"),
24            Severity::Critical => write!(f, "CRITICAL"),
25        }
26    }
27}
28
29/// Category of validation issue
30#[derive(Debug, Clone, PartialEq)]
31pub enum IssueCategory {
32    MissingValue,
33    InvalidFormat,
34    TypeMismatch,
35    ConstraintViolation,
36    Duplicate,
37    Outlier,
38    Custom(String),
39}
40
41/// Represents a single validation issue
42#[derive(Debug, Clone)]
43pub struct ValidationIssue {
44    pub severity: Severity,
45    pub category: IssueCategory,
46    pub message: String,
47    pub error_code: Option<String>,
48    pub field: Option<String>,
49    pub row: Option<usize>,
50    pub column: Option<usize>,
51    pub value: Option<String>,
52    pub suggestion: Option<String>,
53    pub auto_fixed: bool,
54}
55
56impl ValidationIssue {
57    /// Creates a new validation issue
58    pub fn new(severity: Severity, category: IssueCategory, message: impl Into<String>) -> Self {
59        Self {
60            severity,
61            category,
62            message: message.into(),
63            error_code: None,
64            field: None,
65            row: None,
66            column: None,
67            value: None,
68            suggestion: None,
69            auto_fixed: false,
70        }
71    }
72
73    /// Sets the error code for this issue
74    pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
75        self.error_code = Some(code.into());
76        self
77    }
78
79    /// Marks this issue as auto-fixed
80    pub fn mark_auto_fixed(mut self) -> Self {
81        self.auto_fixed = true;
82        self
83    }
84
85    /// Sets the field name for this issue
86    pub fn with_field(mut self, field: impl Into<String>) -> Self {
87        self.field = Some(field.into());
88        self
89    }
90
91    /// Sets the row number for this issue
92    pub fn with_row(mut self, row: usize) -> Self {
93        self.row = Some(row);
94        self
95    }
96
97    /// Sets the column number for this issue
98    pub fn with_column(mut self, column: usize) -> Self {
99        self.column = Some(column);
100        self
101    }
102
103    /// Sets the problematic value
104    pub fn with_value(mut self, value: impl Into<String>) -> Self {
105        self.value = Some(value.into());
106        self
107    }
108
109    /// Sets a suggestion for fixing the issue
110    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
111        self.suggestion = Some(suggestion.into());
112        self
113    }
114}
115
116impl fmt::Display for ValidationIssue {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "[{}] ", self.severity)?;
119
120        if let Some(row) = self.row {
121            write!(f, "Row {}", row)?;
122            if let Some(col) = self.column {
123                write!(f, ", Col {}", col)?;
124            }
125            write!(f, ": ")?;
126        }
127
128        if let Some(field) = &self.field {
129            write!(f, "Field '{}': ", field)?;
130        }
131
132        write!(f, "{}", self.message)?;
133
134        if let Some(value) = &self.value {
135            write!(f, " (value: '{}')", value)?;
136        }
137
138        Ok(())
139    }
140}
141
142/// Statistics about validation results
143#[derive(Debug, Clone, Default)]
144pub struct ValidationStats {
145    pub total_records: usize,
146    pub valid_records: usize,
147    pub invalid_records: usize,
148    pub total_issues: usize,
149    pub critical_issues: usize,
150    pub error_issues: usize,
151    pub warning_issues: usize,
152    pub info_issues: usize,
153    pub auto_fixed: usize,
154}
155
156impl ValidationStats {
157    /// Creates new empty statistics
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// Updates statistics with a new issue
163    pub fn add_issue(&mut self, issue: &ValidationIssue) {
164        self.total_issues += 1;
165        if issue.auto_fixed {
166            self.auto_fixed += 1;
167        }
168        match issue.severity {
169            Severity::Critical => self.critical_issues += 1,
170            Severity::Error => self.error_issues += 1,
171            Severity::Warning => self.warning_issues += 1,
172            Severity::Info => self.info_issues += 1,
173        }
174    }
175}
176
177/// Complete validation result
178#[derive(Debug, Clone)]
179pub struct ValidationResult {
180    pub success: bool,
181    pub issues: Vec<ValidationIssue>,
182    pub stats: ValidationStats,
183    pub metadata: Option<String>,
184}
185
186impl ValidationResult {
187    /// Creates a new validation result
188    pub fn new(success: bool) -> Self {
189        Self {
190            success,
191            issues: Vec::new(),
192            stats: ValidationStats::new(),
193            metadata: None,
194        }
195    }
196
197    /// Adds an issue to the result
198    pub fn add_issue(&mut self, issue: ValidationIssue) {
199        self.stats.add_issue(&issue);
200        self.issues.push(issue);
201    }
202
203    /// Checks if there are any critical or error issues
204    pub fn has_errors(&self) -> bool {
205        self.stats.critical_issues > 0 || self.stats.error_issues > 0
206    }
207
208    /// Gets issues by severity
209    pub fn issues_by_severity(&self, severity: Severity) -> Vec<&ValidationIssue> {
210        self.issues
211            .iter()
212            .filter(|issue| issue.severity == severity)
213            .collect()
214    }
215}
216
217impl Default for ValidationResult {
218    fn default() -> Self {
219        Self::new(true)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_validation_issue_creation() {
229        let issue = ValidationIssue::new(
230            Severity::Error,
231            IssueCategory::MissingValue,
232            "Field is required",
233        )
234        .with_field("email")
235        .with_row(5);
236
237        assert_eq!(issue.severity, Severity::Error);
238        assert_eq!(issue.field, Some("email".to_string()));
239        assert_eq!(issue.row, Some(5));
240    }
241
242    #[test]
243    fn test_validation_result() {
244        let mut result = ValidationResult::new(false);
245        result.add_issue(ValidationIssue::new(
246            Severity::Error,
247            IssueCategory::InvalidFormat,
248            "Invalid email format",
249        ));
250
251        assert_eq!(result.issues.len(), 1);
252        assert_eq!(result.stats.total_issues, 1);
253        assert_eq!(result.stats.error_issues, 1);
254        assert!(result.has_errors());
255    }
256}