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}