Skip to main content

mx20022_validate/schemes/
mod.rs

1//! Payment scheme-specific validation.
2//!
3//! Each payment scheme (`FedNow`, SEPA, `CBPR+`) has additional rules beyond the
4//! base ISO 20022 schema. This module provides validators that enforce these
5//! scheme-specific constraints.
6//!
7//! # Design
8//!
9//! Scheme validators support two validation paths:
10//!
11//! 1. **XML-based** ([`SchemeValidator::validate`]) — operates on raw XML
12//!    strings using lightweight string scanning ([`xml_scan`]).
13//! 2. **Typed** ([`SchemeValidator::validate_typed`]) — operates on
14//!    deserialized message structs via `std::any::Any` downcasting.
15//!
16//! The typed path is preferred when the caller has already deserialized the
17//! message. It avoids fragile XML string scanning and catches field-level
18//! issues at compile time (within the validator implementation).
19//!
20//! # Error Paths
21//!
22//! Error paths in [`ValidationError`](crate::error::ValidationError) follow
23//! XPath-like conventions:
24//!
25//! | Style | Example | When |
26//! |---|---|---|
27//! | Absolute | `/Document/FIToFICstmrCdtTrf/GrpHdr/MsgId` | Typed path (field known) |
28//! | Abbreviated | `//BICFI` | XML scan (element found anywhere) |
29//! | Root element | `/AppHdr` | Envelope-level checks |
30//!
31//! **New validators should prefer absolute paths** when the field location is
32//! known (always the case for typed validators). Abbreviated `//Element`
33//! paths are acceptable for XML-scan checks that match elements regardless
34//! of position.
35//!
36//! # Usage
37//!
38//! ```rust
39//! use mx20022_validate::schemes::fednow::FedNowValidator;
40//! use mx20022_validate::schemes::SchemeValidator;
41//!
42//! let validator = FedNowValidator::new();
43//! let xml = r#"<?xml version="1.0"?><Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13"></Document>"#;
44//! let result = validator.validate(xml, "pacs.008.001.13");
45//! // Result may contain errors for missing mandatory fields.
46//! println!("{} error(s)", result.error_count());
47//! ```
48
49pub mod cbpr;
50pub(crate) mod common;
51pub mod fednow;
52pub mod sepa;
53pub mod xml_scan;
54
55use std::any::Any;
56
57use crate::error::ValidationResult;
58
59/// A scheme-specific validator for ISO 20022 payment messages.
60///
61/// Provides two validation paths: XML-based ([`validate`](SchemeValidator::validate))
62/// and typed ([`validate_typed`](SchemeValidator::validate_typed)).
63///
64/// # Contract
65///
66/// - `validate` **must** return an empty [`ValidationResult`] (no errors,
67///   no warnings) for message types not listed in
68///   [`supported_messages`](SchemeValidator::supported_messages).
69/// - `validate_typed` returns `None` for unsupported message types or
70///   failed downcasts, and `Some(result)` for actual validation.
71/// - Neither method should panic; callers may provide malformed XML or
72///   unrecognised types.
73/// - Implementations should be `Send + Sync` so they can be stored in
74///   `Arc<dyn SchemeValidator>`.
75pub trait SchemeValidator: Send + Sync {
76    /// Human-readable name of the scheme (e.g. `"FedNow"`, `"SEPA"`, `"CBPR+"`).
77    fn name(&self) -> &'static str;
78
79    /// Short message type identifiers supported by this scheme.
80    ///
81    /// Each entry is a two-segment dot-separated identifier such as
82    /// `"pacs.008"` or `"camt.056"`.  The validator should ignore messages
83    /// whose type does not appear in this list.
84    fn supported_messages(&self) -> &[&str];
85
86    /// Validate raw XML content against this scheme's rules.
87    ///
88    /// # Migration
89    ///
90    /// This method operates on raw XML strings using fragile string scanning.
91    /// **New callers should use [`validate_typed`](SchemeValidator::validate_typed)**
92    /// which operates on deserialized message structs and catches field-level
93    /// issues at compile time.
94    ///
95    /// The XML-based path remains available for cases where raw XML is the
96    /// only input (e.g. CLI validation without prior deserialization), or for
97    /// checks that inherently require raw XML (message size, `AppHdr` envelope,
98    /// control characters).
99    ///
100    /// `message_type` is the full ISO 20022 message type detected from the
101    /// XML namespace (e.g. `"pacs.008.001.13"`).  The validator is responsible
102    /// for deriving the short type and returning early for unsupported types.
103    fn validate(&self, xml: &str, message_type: &str) -> ValidationResult;
104
105    /// Validate a typed (deserialized) message against this scheme's rules.
106    ///
107    /// `msg` is a reference to the deserialized message struct (e.g.
108    /// `pacs_008_001_13::Document`). Implementations downcast via
109    /// `Any::downcast_ref` to the concrete types they support.
110    ///
111    /// `message_type` is the full ISO 20022 message type (e.g.
112    /// `"pacs.008.001.13"`), used to route to the appropriate validation logic.
113    ///
114    /// Returns `Some(result)` when the validator supports the given message
115    /// type and the downcast succeeds. Returns `None` for unsupported message
116    /// types or failed downcasts, allowing callers to distinguish "valid with
117    /// no errors" from "not applicable".
118    fn validate_typed(&self, msg: &dyn Any, message_type: &str) -> Option<ValidationResult> {
119        let _ = (msg, message_type);
120        None
121    }
122}
123
124/// Extract the short message type (e.g. `"pacs.008"`) from a full type
125/// string like `"pacs.008.001.13"`.
126pub fn short_message_type(message_type: &str) -> String {
127    message_type
128        .splitn(3, '.')
129        .take(2)
130        .collect::<Vec<_>>()
131        .join(".")
132}