Skip to main content

mx20022_validate/
error.rs

1//! Validation error types for ISO 20022 message validation.
2
3/// A single validation finding at a specific location in a message.
4#[derive(Debug, Clone)]
5pub struct ValidationError {
6    /// XPath-like location in the message (e.g. `/Document/FIToFICstmrCdtTrf/GrpHdr/MsgId`).
7    pub path: String,
8    /// Severity of the finding.
9    pub severity: Severity,
10    /// Rule identifier (e.g. `IBAN_CHECK`, `MAX_LENGTH_35`).
11    pub rule_id: String,
12    /// Human-readable description.
13    pub message: String,
14}
15
16impl ValidationError {
17    /// Create a new [`ValidationError`].
18    ///
19    /// # Examples
20    ///
21    /// ```
22    /// use mx20022_validate::error::{ValidationError, Severity};
23    ///
24    /// let err = ValidationError::new("/Document/MsgId", Severity::Error, "MAX_LENGTH_35", "Value exceeds maximum length of 35");
25    /// assert_eq!(err.severity, Severity::Error);
26    /// ```
27    pub fn new(
28        path: impl Into<String>,
29        severity: Severity,
30        rule_id: impl Into<String>,
31        message: impl Into<String>,
32    ) -> Self {
33        Self {
34            path: path.into(),
35            severity,
36            rule_id: rule_id.into(),
37            message: message.into(),
38        }
39    }
40}
41
42/// Severity level of a validation finding.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Severity {
45    /// A hard error — the message is not valid.
46    Error,
47    /// A warning — the message may be valid but has a notable issue.
48    Warning,
49    /// Informational — for diagnostics only.
50    Info,
51}
52
53/// The aggregated result of validating an ISO 20022 message.
54///
55/// # Examples
56///
57/// ```
58/// use mx20022_validate::error::{ValidationResult, ValidationError, Severity};
59///
60/// let result = ValidationResult::new(vec![]);
61/// assert!(result.is_valid());
62/// assert_eq!(result.error_count(), 0);
63/// assert_eq!(result.warning_count(), 0);
64/// ```
65#[derive(Debug, Clone)]
66pub struct ValidationResult {
67    /// All findings produced during validation.
68    pub errors: Vec<ValidationError>,
69}
70
71impl ValidationResult {
72    /// Create a new `ValidationResult` from a list of findings.
73    pub fn new(errors: Vec<ValidationError>) -> Self {
74        Self { errors }
75    }
76
77    /// Returns `true` if there are no findings with [`Severity::Error`].
78    pub fn is_valid(&self) -> bool {
79        !self.errors.iter().any(|e| e.severity == Severity::Error)
80    }
81
82    /// Count of findings with [`Severity::Error`].
83    pub fn error_count(&self) -> usize {
84        self.errors
85            .iter()
86            .filter(|e| e.severity == Severity::Error)
87            .count()
88    }
89
90    /// Count of findings with [`Severity::Warning`].
91    pub fn warning_count(&self) -> usize {
92        self.errors
93            .iter()
94            .filter(|e| e.severity == Severity::Warning)
95            .count()
96    }
97
98    /// Merge another `ValidationResult` into this one.
99    pub fn merge(&mut self, other: ValidationResult) {
100        self.errors.extend(other.errors);
101    }
102}
103
104impl Default for ValidationResult {
105    fn default() -> Self {
106        Self::new(vec![])
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn is_valid_with_no_errors() {
116        let result = ValidationResult::new(vec![]);
117        assert!(result.is_valid());
118    }
119
120    #[test]
121    fn is_invalid_with_error() {
122        let result = ValidationResult::new(vec![ValidationError::new(
123            "/path",
124            Severity::Error,
125            "RULE_1",
126            "Some error",
127        )]);
128        assert!(!result.is_valid());
129    }
130
131    #[test]
132    fn is_valid_with_only_warning() {
133        let result = ValidationResult::new(vec![ValidationError::new(
134            "/path",
135            Severity::Warning,
136            "RULE_1",
137            "Some warning",
138        )]);
139        assert!(result.is_valid());
140        assert_eq!(result.warning_count(), 1);
141        assert_eq!(result.error_count(), 0);
142    }
143
144    #[test]
145    fn counts_are_correct() {
146        let result = ValidationResult::new(vec![
147            ValidationError::new("/a", Severity::Error, "R1", "e1"),
148            ValidationError::new("/b", Severity::Error, "R2", "e2"),
149            ValidationError::new("/c", Severity::Warning, "R3", "w1"),
150            ValidationError::new("/d", Severity::Info, "R4", "i1"),
151        ]);
152        assert_eq!(result.error_count(), 2);
153        assert_eq!(result.warning_count(), 1);
154        assert!(!result.is_valid());
155    }
156
157    #[test]
158    fn merge_combines_findings() {
159        let mut a = ValidationResult::new(vec![ValidationError::new(
160            "/a",
161            Severity::Error,
162            "R1",
163            "e1",
164        )]);
165        let b = ValidationResult::new(vec![ValidationError::new(
166            "/b",
167            Severity::Warning,
168            "R2",
169            "w1",
170        )]);
171        a.merge(b);
172        assert_eq!(a.errors.len(), 2);
173        assert_eq!(a.error_count(), 1);
174        assert_eq!(a.warning_count(), 1);
175    }
176}