sara_core/validation/
report.rs

1//! Validation report structure.
2
3use std::time::Duration;
4
5use serde::Serialize;
6
7use crate::error::ValidationError;
8
9/// Severity level for validation issues.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "lowercase")]
12pub enum Severity {
13    /// Validation error that blocks acceptance.
14    Error,
15    /// Warning that doesn't block but should be addressed.
16    Warning,
17}
18
19/// A validation issue with its severity.
20#[derive(Debug, Clone, Serialize)]
21pub struct ValidationIssue {
22    /// Severity of the issue.
23    pub severity: Severity,
24    /// The underlying validation error.
25    pub error: ValidationError,
26}
27
28impl ValidationIssue {
29    /// Creates a new error-level issue.
30    pub fn error(error: ValidationError) -> Self {
31        Self {
32            severity: Severity::Error,
33            error,
34        }
35    }
36
37    /// Creates a new warning-level issue.
38    pub fn warning(error: ValidationError) -> Self {
39        Self {
40            severity: Severity::Warning,
41            error,
42        }
43    }
44}
45
46/// Validation report containing all issues found.
47#[derive(Debug, Clone, Serialize)]
48pub struct ValidationReport {
49    /// All validation issues found.
50    pub issues: Vec<ValidationIssue>,
51    /// Number of items checked.
52    pub items_checked: usize,
53    /// Number of relationships checked.
54    pub relationships_checked: usize,
55    /// Time taken to perform validation.
56    #[serde(skip)]
57    pub duration: Duration,
58}
59
60impl ValidationReport {
61    /// Creates a new empty validation report.
62    pub fn new() -> Self {
63        Self {
64            issues: Vec::new(),
65            items_checked: 0,
66            relationships_checked: 0,
67            duration: Duration::ZERO,
68        }
69    }
70
71    /// Returns true if validation passed (no errors).
72    pub fn is_valid(&self) -> bool {
73        !self.issues.iter().any(|i| i.severity == Severity::Error)
74    }
75
76    /// Returns the number of errors.
77    pub fn error_count(&self) -> usize {
78        self.issues
79            .iter()
80            .filter(|i| i.severity == Severity::Error)
81            .count()
82    }
83
84    /// Returns the number of warnings.
85    pub fn warning_count(&self) -> usize {
86        self.issues
87            .iter()
88            .filter(|i| i.severity == Severity::Warning)
89            .count()
90    }
91
92    /// Returns all errors.
93    pub fn errors(&self) -> Vec<&ValidationError> {
94        self.issues
95            .iter()
96            .filter(|i| i.severity == Severity::Error)
97            .map(|i| &i.error)
98            .collect()
99    }
100
101    /// Returns all warnings.
102    pub fn warnings(&self) -> Vec<&ValidationError> {
103        self.issues
104            .iter()
105            .filter(|i| i.severity == Severity::Warning)
106            .map(|i| &i.error)
107            .collect()
108    }
109
110    /// Adds an error to the report.
111    pub fn add_error(&mut self, error: ValidationError) {
112        self.issues.push(ValidationIssue::error(error));
113    }
114
115    /// Adds a warning to the report.
116    pub fn add_warning(&mut self, error: ValidationError) {
117        self.issues.push(ValidationIssue::warning(error));
118    }
119
120    /// Adds multiple errors to the report.
121    pub fn add_errors(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
122        for error in errors {
123            self.add_error(error);
124        }
125    }
126
127    /// Adds multiple warnings to the report.
128    pub fn add_warnings(&mut self, errors: impl IntoIterator<Item = ValidationError>) {
129        for error in errors {
130            self.add_warning(error);
131        }
132    }
133}
134
135impl Default for ValidationReport {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141/// Builder for creating validation reports.
142#[derive(Debug, Default)]
143pub struct ValidationReportBuilder {
144    report: ValidationReport,
145}
146
147impl ValidationReportBuilder {
148    /// Creates a new builder.
149    pub fn new() -> Self {
150        Self::default()
151    }
152
153    /// Sets the number of items checked.
154    pub fn items_checked(mut self, count: usize) -> Self {
155        self.report.items_checked = count;
156        self
157    }
158
159    /// Sets the number of relationships checked.
160    pub fn relationships_checked(mut self, count: usize) -> Self {
161        self.report.relationships_checked = count;
162        self
163    }
164
165    /// Sets the duration.
166    pub fn duration(mut self, duration: Duration) -> Self {
167        self.report.duration = duration;
168        self
169    }
170
171    /// Adds an error.
172    pub fn error(mut self, error: ValidationError) -> Self {
173        self.report.add_error(error);
174        self
175    }
176
177    /// Adds a warning.
178    pub fn warning(mut self, error: ValidationError) -> Self {
179        self.report.add_warning(error);
180        self
181    }
182
183    /// Adds multiple errors.
184    pub fn errors(mut self, errors: impl IntoIterator<Item = ValidationError>) -> Self {
185        self.report.add_errors(errors);
186        self
187    }
188
189    /// Adds multiple warnings.
190    pub fn warnings(mut self, errors: impl IntoIterator<Item = ValidationError>) -> Self {
191        self.report.add_warnings(errors);
192        self
193    }
194
195    /// Builds the report.
196    pub fn build(self) -> ValidationReport {
197        self.report
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::model::ItemId;
205
206    #[test]
207    fn test_empty_report_is_valid() {
208        let report = ValidationReport::new();
209        assert!(report.is_valid());
210        assert_eq!(report.error_count(), 0);
211        assert_eq!(report.warning_count(), 0);
212    }
213
214    #[test]
215    fn test_report_with_error() {
216        let mut report = ValidationReport::new();
217        report.add_error(ValidationError::BrokenReference {
218            from: ItemId::new_unchecked("A"),
219            to: ItemId::new_unchecked("B"),
220            location: None,
221        });
222
223        assert!(!report.is_valid());
224        assert_eq!(report.error_count(), 1);
225        assert_eq!(report.warning_count(), 0);
226    }
227
228    #[test]
229    fn test_report_with_warning() {
230        let mut report = ValidationReport::new();
231        report.add_warning(ValidationError::OrphanItem {
232            id: ItemId::new_unchecked("A"),
233            item_type: crate::model::ItemType::UseCase,
234            location: None,
235        });
236
237        assert!(report.is_valid());
238        assert_eq!(report.error_count(), 0);
239        assert_eq!(report.warning_count(), 1);
240    }
241
242    #[test]
243    fn test_builder() {
244        let report = ValidationReportBuilder::new()
245            .items_checked(10)
246            .relationships_checked(15)
247            .error(ValidationError::BrokenReference {
248                from: ItemId::new_unchecked("A"),
249                to: ItemId::new_unchecked("B"),
250                location: None,
251            })
252            .build();
253
254        assert_eq!(report.items_checked, 10);
255        assert_eq!(report.relationships_checked, 15);
256        assert_eq!(report.error_count(), 1);
257    }
258}