Skip to main content

mx20022_validate/schema/
mod.rs

1//! Schema-level validation.
2//!
3//! This module provides the thin orchestration layer that drives field-level
4//! constraint checking through the [`RuleRegistry`]. The actual rule logic lives
5//! in [`crate::rules`]; the constraint definitions for a particular message schema
6//! are expressed as [`constraints::ConstraintSet`] instances.
7//!
8//! # Design
9//!
10//! ```text
11//! ConstraintSet ──► RuleRegistry ──► Rule::validate()
12//!                                         │
13//!                                         ▼
14//!                                  Vec<ValidationError>
15//! ```
16//!
17//! [`SchemaValidator`] wraps a [`ConstraintSet`] + [`RuleRegistry`] pair and
18//! exposes a single [`SchemaValidator::validate_field`] entry-point.
19
20pub mod constraints;
21
22use crate::error::ValidationResult;
23use crate::rules::RuleRegistry;
24use constraints::ConstraintSet;
25
26/// Orchestrates schema-level validation for a message type.
27///
28/// # Examples
29///
30/// ```
31/// use mx20022_validate::schema::{SchemaValidator};
32/// use mx20022_validate::schema::constraints::{ConstraintSet, FieldConstraint};
33/// use mx20022_validate::rules::RuleRegistry;
34///
35/// let mut cs = ConstraintSet::new();
36/// cs.add(FieldConstraint::new("/Document/GrpHdr/MsgId", ["MAX_LENGTH"]));
37///
38/// let validator = SchemaValidator::new(cs, RuleRegistry::with_defaults());
39/// let result = validator.validate_field("/Document/GrpHdr/MsgId", "ABC123");
40/// assert!(result.is_valid());
41/// ```
42pub struct SchemaValidator {
43    constraints: ConstraintSet,
44    registry: RuleRegistry,
45}
46
47impl SchemaValidator {
48    /// Create a new `SchemaValidator` from a constraint set and rule registry.
49    pub fn new(constraints: ConstraintSet, registry: RuleRegistry) -> Self {
50        Self {
51            constraints,
52            registry,
53        }
54    }
55
56    /// Create a `SchemaValidator` with an empty constraint set and the default rule registry.
57    pub fn with_defaults() -> Self {
58        Self::new(ConstraintSet::new(), RuleRegistry::with_defaults())
59    }
60
61    /// Validate a single field value at the given path.
62    ///
63    /// Returns a [`ValidationResult`] with any findings from constraints registered
64    /// for this path. If no constraints are registered for the path the result is
65    /// unconditionally valid.
66    pub fn validate_field(&self, path: &str, value: &str) -> ValidationResult {
67        self.constraints.validate_field(path, value, &self.registry)
68    }
69
70    /// Access the underlying registry (e.g. to register additional rules).
71    pub fn registry_mut(&mut self) -> &mut RuleRegistry {
72        &mut self.registry
73    }
74
75    /// Access the underlying constraint set (e.g. to add constraints at runtime).
76    pub fn constraints_mut(&mut self) -> &mut ConstraintSet {
77        &mut self.constraints
78    }
79
80    /// Validate multiple `(path, value)` pairs and merge all findings.
81    pub fn validate_fields<'a>(
82        &self,
83        fields: impl IntoIterator<Item = (&'a str, &'a str)>,
84    ) -> ValidationResult {
85        let mut result = ValidationResult::default();
86        for (path, value) in fields {
87            result.merge(self.validate_field(path, value));
88        }
89        result
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use constraints::FieldConstraint;
97
98    #[test]
99    fn with_defaults_produces_valid_for_unconstrained_path() {
100        let validator = SchemaValidator::with_defaults();
101        let result = validator.validate_field("/any/path", "any value");
102        assert!(result.is_valid());
103    }
104
105    #[test]
106    fn validate_field_iban() {
107        let mut cs = ConstraintSet::new();
108        cs.add(FieldConstraint::new("/iban", ["IBAN_CHECK"]));
109        let validator = SchemaValidator::new(cs, RuleRegistry::with_defaults());
110
111        assert!(validator
112            .validate_field("/iban", "GB82WEST12345698765432")
113            .is_valid());
114        assert!(!validator.validate_field("/iban", "NOTANIBAN").is_valid());
115    }
116
117    #[test]
118    fn validate_fields_merges_results() {
119        let mut cs = ConstraintSet::new();
120        cs.add(FieldConstraint::new("/iban", ["IBAN_CHECK"]));
121        cs.add(FieldConstraint::new("/bic", ["BIC_CHECK"]));
122        let validator = SchemaValidator::new(cs, RuleRegistry::with_defaults());
123
124        let result = validator.validate_fields([("/iban", "NOTANIBAN"), ("/bic", "NOTABIC")]);
125        // Both fields should produce errors
126        assert!(!result.is_valid());
127        assert!(result.error_count() >= 2);
128    }
129
130    #[test]
131    fn validate_fields_all_valid() {
132        let mut cs = ConstraintSet::new();
133        cs.add(FieldConstraint::new("/iban", ["IBAN_CHECK"]));
134        cs.add(FieldConstraint::new("/bic", ["BIC_CHECK"]));
135        let validator = SchemaValidator::new(cs, RuleRegistry::with_defaults());
136
137        let result =
138            validator.validate_fields([("/iban", "GB82WEST12345698765432"), ("/bic", "AAAAGB2L")]);
139        assert!(result.is_valid());
140    }
141}