Skip to main content

mx20022_parse/
de.rs

1//! Deserialization of ISO 20022 XML messages.
2//!
3//! Thin wrappers around [`quick_xml::de`] that translate errors into
4//! [`ParseError`].
5//!
6//! # Examples
7//!
8//! ```no_run
9//! # use mx20022_parse::de::from_str;
10//! # use mx20022_model::generated::head::BusinessApplicationHeaderV04;
11//! let xml = r#"<AppHdr xmlns="urn:iso:std:iso:20022:tech:xsd:head.001.001.04">...</AppHdr>"#;
12//! let _hdr: BusinessApplicationHeaderV04 = from_str(xml).unwrap();
13//! ```
14
15use serde::de::DeserializeOwned;
16
17use crate::{envelope::detect_message_type, ParseError};
18
19/// Deserialize an ISO 20022 XML message from a string slice.
20///
21/// The root element name must match the serde rename of the target type.
22///
23/// # Errors
24///
25/// Returns [`ParseError::Deserialize`] if the XML is malformed or does not
26/// match the expected schema.
27pub fn from_str<T: DeserializeOwned>(xml: &str) -> Result<T, ParseError> {
28    quick_xml::de::from_str(xml).map_err(ParseError::Deserialize)
29}
30
31/// Deserialize an ISO 20022 XML message from a buffered reader.
32///
33/// # Errors
34///
35/// Returns [`ParseError::Deserialize`] if the XML is malformed or does not
36/// match the expected schema.
37pub fn from_reader<R: std::io::BufRead, T: DeserializeOwned>(reader: R) -> Result<T, ParseError> {
38    quick_xml::de::from_reader(reader).map_err(ParseError::Deserialize)
39}
40
41/// Deserialize an ISO 20022 XML message and tag any deserialization
42/// failure with the detected message identifier.
43///
44/// Behaves like [`from_str`] on success. On failure, if the envelope
45/// detection succeeded the error is returned as
46/// [`ParseError::DeserializeIn`] carrying the dotted message ID
47/// (e.g. `"pacs.008.001.13"`); if envelope detection itself failed,
48/// the underlying `quick_xml` error is returned via
49/// [`ParseError::Deserialize`] unchanged.
50///
51/// Useful when the XML may be one of several pacs/pain/camt/head
52/// messages and an opaque `quick_xml` diagnostic alone is not enough
53/// to tell the user *which* schema mismatched.
54///
55/// # Errors
56///
57/// Returns [`ParseError::DeserializeIn`] when envelope detection
58/// succeeded but deserialization failed; returns
59/// [`ParseError::Deserialize`] otherwise.
60pub fn from_str_in_envelope<T: DeserializeOwned>(xml: &str) -> Result<T, ParseError> {
61    let detected = detect_message_type(xml).ok();
62    quick_xml::de::from_str(xml).map_err(|e| match detected {
63        Some(id) => ParseError::DeserializeIn {
64            context: id.dotted(),
65            source: e,
66        },
67        None => ParseError::Deserialize(e),
68    })
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    /// A struct that requires XML element structure to deserialize.
76    #[derive(serde::Deserialize, Debug)]
77    struct Foo {
78        #[allow(dead_code)]
79        x: u32,
80    }
81
82    #[test]
83    fn from_str_invalid_xml_returns_error() {
84        // "<<<garbage>>>" is invalid XML and cannot match the Foo struct shape.
85        let result: Result<Foo, _> = from_str("<<<garbage>>>");
86        assert!(result.is_err(), "malformed XML must return an error");
87    }
88
89    #[test]
90    fn from_reader_invalid_xml_returns_error() {
91        let xml = b"<<<garbage>>>";
92        let result: Result<Foo, _> = from_reader(xml.as_ref());
93        assert!(result.is_err(), "malformed XML must return an error");
94    }
95
96    #[test]
97    fn from_str_in_envelope_includes_detected_message_id_on_failure() {
98        // Valid pacs.008 envelope, but the body shape does not match Foo
99        // (no <x> element).
100        let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13"><Other/></Document>"#;
101        let err = from_str_in_envelope::<Foo>(xml).unwrap_err();
102        match err {
103            ParseError::DeserializeIn { ref context, .. } => {
104                assert_eq!(context, "pacs.008.001.13");
105                let rendered = err.to_string();
106                assert!(
107                    rendered.contains("pacs.008.001.13"),
108                    "Display should mention the detected envelope, got: {rendered}"
109                );
110            }
111            other => panic!("expected DeserializeIn, got: {other:?}"),
112        }
113    }
114
115    #[test]
116    fn from_str_in_envelope_falls_back_to_plain_deserialize_when_envelope_unknown() {
117        // No ISO 20022 namespace → detection fails, error stays as
118        // ParseError::Deserialize so existing match-arms keep working.
119        let xml = r#"<Document><Other/></Document>"#;
120        let err = from_str_in_envelope::<Foo>(xml).unwrap_err();
121        assert!(
122            matches!(err, ParseError::Deserialize(_)),
123            "expected Deserialize without context, got: {err:?}"
124        );
125    }
126
127    #[test]
128    fn from_str_in_envelope_succeeds_passes_through() {
129        // Foo has a single u32 field `x`. Wrap it in a pacs.008 envelope.
130        let xml = r#"<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pacs.008.001.13"><x>42</x></Document>"#;
131        // Foo's deserializer reads <x>; whether quick_xml accepts the
132        // shape depends on the structural decisions of serde-xml-rs vs
133        // quick_xml. The behaviour we want to assert is "no
134        // DeserializeIn was raised when deserialization succeeded" — the
135        // call returns Ok or returns Deserialize/DeserializeIn but never
136        // panics. In practice the body matches, so:
137        let v: Foo = from_str_in_envelope(xml).expect("body matches Foo");
138        assert_eq!(v.x, 42);
139    }
140}