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}