1use std::fmt;
8
9#[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#[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#[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 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 pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
75 self.error_code = Some(code.into());
76 self
77 }
78
79 pub fn mark_auto_fixed(mut self) -> Self {
81 self.auto_fixed = true;
82 self
83 }
84
85 pub fn with_field(mut self, field: impl Into<String>) -> Self {
87 self.field = Some(field.into());
88 self
89 }
90
91 pub fn with_row(mut self, row: usize) -> Self {
93 self.row = Some(row);
94 self
95 }
96
97 pub fn with_column(mut self, column: usize) -> Self {
99 self.column = Some(column);
100 self
101 }
102
103 pub fn with_value(mut self, value: impl Into<String>) -> Self {
105 self.value = Some(value.into());
106 self
107 }
108
109 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#[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 pub fn new() -> Self {
159 Self::default()
160 }
161
162 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#[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 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 pub fn add_issue(&mut self, issue: ValidationIssue) {
199 self.stats.add_issue(&issue);
200 self.issues.push(issue);
201 }
202
203 pub fn has_errors(&self) -> bool {
205 self.stats.critical_issues > 0 || self.stats.error_issues > 0
206 }
207
208 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}