Skip to main content

mx20022_model/common/
validate.rs

1//! Validation trait and types for ISO 20022 generated types.
2//!
3//! The [`Validatable`] trait enables generated types to self-validate against
4//! their XSD constraints (pattern, length, range, etc.). Implementations are
5//! generated by the `mx20022-codegen` tool based on XSD restriction annotations.
6//!
7//! Struct types recurse into their fields; newtypes check their inner value
8//! against known constraints; code enums and opaque types are no-ops.
9//!
10//! # Example
11//!
12//! ```no_run
13//! use mx20022_model::common::validate::{Validatable, ConstraintViolation};
14//!
15//! // Generated types implement Validatable:
16//! # struct Max35Text(pub String);
17//! # impl Validatable for Max35Text {
18//! #     fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
19//! #         if self.0.chars().count() > 35 {
20//! #             violations.push(ConstraintViolation {
21//! #                 path: path.to_string(),
22//! #                 message: "exceeds maximum length 35".to_string(),
23//! #                 kind: mx20022_model::common::validate::ConstraintKind::MaxLength,
24//! #             });
25//! #         }
26//! #     }
27//! # }
28//!
29//! let val = Max35Text("short".to_string());
30//! let mut violations = Vec::new();
31//! val.validate_constraints("/Path/Field", &mut violations);
32//! assert!(violations.is_empty());
33//! ```
34
35/// A violation of an XSD-level constraint.
36///
37/// Produced by [`Validatable::validate_constraints`] when a field value does
38/// not satisfy its schema-defined restrictions.
39#[derive(Debug, Clone, PartialEq)]
40pub struct ConstraintViolation {
41    /// `XPath`-like path to the violating field (e.g.
42    /// `"/Document/FIToFICstmrCdtTrf/GrpHdr/MsgId"`).
43    pub path: String,
44    /// Human-readable description of the violation.
45    pub message: String,
46    /// Which constraint kind was violated.
47    pub kind: ConstraintKind,
48}
49
50/// Error returned when constructing a constrained newtype with an invalid value.
51///
52/// Produced by `TryFrom<String>` and `new()` on generated newtypes that have
53/// XSD constraints (pattern, length, digits).
54#[derive(Debug, Clone, PartialEq)]
55pub struct ConstraintError {
56    /// Which constraint kind was violated.
57    pub kind: ConstraintKind,
58    /// Human-readable description of the violation.
59    pub message: String,
60}
61
62impl core::fmt::Display for ConstraintError {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        f.write_str(&self.message)
65    }
66}
67
68impl std::error::Error for ConstraintError {}
69
70/// The kind of XSD constraint that was violated.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ConstraintKind {
73    /// `xs:minLength` — value is shorter than required.
74    MinLength,
75    /// `xs:maxLength` — value exceeds maximum length.
76    MaxLength,
77    /// `xs:pattern` — value does not match the required regex.
78    Pattern,
79    /// `xs:minInclusive` — value is below the minimum.
80    MinInclusive,
81    /// `xs:maxInclusive` — value exceeds the maximum.
82    MaxInclusive,
83    /// `xs:totalDigits` — value has too many digits.
84    TotalDigits,
85    /// `xs:fractionDigits` — value has too many fractional digits.
86    FractionDigits,
87}
88
89/// Types that can self-validate against their XSD-level constraints.
90///
91/// Implementations are auto-generated by `mx20022-codegen`. Each type checks
92/// its own constraints and recurses into nested types, accumulating all
93/// violations into the provided `Vec`.
94///
95/// The `path` parameter is the `XPath` prefix for error reporting. Recursive
96/// calls append the current field's XML name to build a full path like
97/// `"/Document/FIToFICstmrCdtTrf/GrpHdr/MsgId"`.
98pub trait Validatable {
99    /// Validate XSD constraints on this value and all nested children.
100    ///
101    /// `path` is the `XPath`-like location of this value in the message tree.
102    /// Violations are appended to `violations`.
103    fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>);
104}
105
106/// Marker trait for document-level types that represent a complete ISO 20022
107/// message.
108///
109/// Only top-level `Document` structs implement this trait. It provides
110/// the message type identifier and root `XPath` used as the base path for
111/// constraint validation.
112pub trait IsoMessage: Validatable {
113    /// The ISO 20022 message type identifier (e.g. `"pacs.008.001.13"`).
114    fn message_type(&self) -> &'static str;
115
116    /// The root XML element path (e.g. `"/Document"`).
117    fn root_path(&self) -> &'static str;
118
119    /// Validate all XSD constraints starting from the root path.
120    ///
121    /// Convenience method that calls [`Validatable::validate_constraints`]
122    /// with the correct root path.
123    fn validate_message(&self) -> Vec<ConstraintViolation> {
124        let mut violations = Vec::new();
125        self.validate_constraints(self.root_path(), &mut violations);
126        violations
127    }
128}
129
130// ── Blanket impls for standard wrappers ──────────────────────────────────────
131
132impl<T: Validatable> Validatable for Option<T> {
133    fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
134        if let Some(ref inner) = self {
135            inner.validate_constraints(path, violations);
136        }
137    }
138}
139
140impl<T: Validatable> Validatable for Vec<T> {
141    fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
142        for (i, item) in self.iter().enumerate() {
143            let snap = violations.len();
144            item.validate_constraints("", violations);
145            if violations.len() > snap {
146                let pfx = format!("{path}[{i}]");
147                for v in &mut violations[snap..] {
148                    v.path.insert_str(0, &pfx);
149                }
150            }
151        }
152    }
153}
154
155/// No-op `Validatable` for `String` (no XSD constraints on bare strings).
156impl Validatable for String {
157    fn validate_constraints(&self, _path: &str, _violations: &mut Vec<ConstraintViolation>) {}
158}
159
160/// No-op `Validatable` for `bool`.
161impl Validatable for bool {
162    fn validate_constraints(&self, _path: &str, _violations: &mut Vec<ConstraintViolation>) {}
163}
164
165impl<T: Validatable> Validatable for super::ChoiceWrapper<T> {
166    fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
167        self.inner.validate_constraints(path, violations);
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn constraint_error_display() {
177        let err = ConstraintError {
178            kind: ConstraintKind::Pattern,
179            message: "value does not match pattern [A-Z]{3,3}".to_string(),
180        };
181        assert_eq!(err.to_string(), "value does not match pattern [A-Z]{3,3}");
182    }
183
184    #[test]
185    fn constraint_error_debug() {
186        let err = ConstraintError {
187            kind: ConstraintKind::MaxLength,
188            message: "too long".to_string(),
189        };
190        let debug = format!("{err:?}");
191        assert!(debug.contains("ConstraintError"));
192        assert!(debug.contains("MaxLength"));
193    }
194
195    #[test]
196    fn constraint_error_eq() {
197        let a = ConstraintError {
198            kind: ConstraintKind::MinLength,
199            message: "too short".to_string(),
200        };
201        let b = ConstraintError {
202            kind: ConstraintKind::MinLength,
203            message: "too short".to_string(),
204        };
205        let c = ConstraintError {
206            kind: ConstraintKind::MaxLength,
207            message: "too short".to_string(),
208        };
209        assert_eq!(a, b);
210        assert_ne!(a, c);
211    }
212
213    #[test]
214    fn constraint_error_is_std_error() {
215        let err = ConstraintError {
216            kind: ConstraintKind::Pattern,
217            message: "bad pattern".to_string(),
218        };
219        let _: &dyn std::error::Error = &err;
220    }
221
222    // ── Blanket impl tests ───────────────────────────────────────────────
223
224    /// Test helper: a type that always produces one violation.
225    struct AlwaysViolates;
226    impl Validatable for AlwaysViolates {
227        fn validate_constraints(&self, path: &str, violations: &mut Vec<ConstraintViolation>) {
228            violations.push(ConstraintViolation {
229                path: path.to_string(),
230                message: "always fails".to_string(),
231                kind: ConstraintKind::Pattern,
232            });
233        }
234    }
235
236    #[test]
237    fn option_none_produces_no_violations() {
238        let val: Option<AlwaysViolates> = None;
239        let mut v = vec![];
240        val.validate_constraints("/root", &mut v);
241        assert!(v.is_empty());
242    }
243
244    #[test]
245    fn option_some_delegates_with_path() {
246        let val: Option<AlwaysViolates> = Some(AlwaysViolates);
247        let mut v = vec![];
248        val.validate_constraints("/root/field", &mut v);
249        assert_eq!(v.len(), 1);
250        assert_eq!(v[0].path, "/root/field");
251    }
252
253    #[test]
254    fn vec_empty_produces_no_violations() {
255        let val: Vec<AlwaysViolates> = vec![];
256        let mut v = vec![];
257        val.validate_constraints("/root", &mut v);
258        assert!(v.is_empty());
259    }
260
261    #[test]
262    fn vec_indexes_path_correctly() {
263        let val = vec![AlwaysViolates, AlwaysViolates, AlwaysViolates];
264        let mut v = vec![];
265        val.validate_constraints("/root/items", &mut v);
266        assert_eq!(v.len(), 3);
267        assert_eq!(v[0].path, "/root/items[0]");
268        assert_eq!(v[1].path, "/root/items[1]");
269        assert_eq!(v[2].path, "/root/items[2]");
270    }
271
272    #[test]
273    fn choice_wrapper_delegates_with_same_path() {
274        let val = super::super::ChoiceWrapper {
275            inner: AlwaysViolates,
276        };
277        let mut v = vec![];
278        val.validate_constraints("/root/choice", &mut v);
279        assert_eq!(v.len(), 1);
280        assert_eq!(v[0].path, "/root/choice");
281    }
282
283    #[test]
284    fn string_produces_no_violations() {
285        let val = "hello".to_string();
286        let mut v = vec![];
287        val.validate_constraints("/root", &mut v);
288        assert!(v.is_empty());
289    }
290}