mx_message/
xml.rs

1// XML Serialization and Deserialization utilities for MX Messages
2
3use crate::mx_envelope::MxEnvelope;
4use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
5use quick_xml::writer::Writer;
6use quick_xml::{de::from_str as xml_from_str, se::to_string as xml_to_string};
7use serde::{Deserialize, Serialize};
8use std::error::Error;
9use std::fmt;
10use std::io::Cursor;
11
12/// Configuration for XML serialization
13#[derive(Debug, Clone)]
14pub struct XmlConfig {
15    /// Whether to include XML declaration
16    pub include_declaration: bool,
17    /// Whether to format with indentation
18    pub pretty_print: bool,
19    /// Indentation string (e.g., "  " for 2 spaces)
20    pub indent: String,
21    /// Whether to include schema location
22    pub include_schema_location: bool,
23}
24
25impl Default for XmlConfig {
26    fn default() -> Self {
27        Self {
28            include_declaration: true,
29            pretty_print: true,
30            indent: "  ".to_string(),
31            include_schema_location: false,
32        }
33    }
34}
35
36/// Error type for XML operations
37#[derive(Debug)]
38pub enum XmlError {
39    SerializationError(String),
40    DeserializationError(String),
41    ValidationError(String),
42}
43
44impl fmt::Display for XmlError {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            XmlError::SerializationError(msg) => write!(f, "XML Serialization Error: {msg}"),
48            XmlError::DeserializationError(msg) => write!(f, "XML Deserialization Error: {msg}"),
49            XmlError::ValidationError(msg) => write!(f, "XML Validation Error: {msg}"),
50        }
51    }
52}
53
54impl Error for XmlError {}
55
56/// Serialize any MX message to complete XML with envelope
57pub fn to_mx_xml<H, D>(
58    message: D,
59    header: H,
60    message_type: &str,
61    config: Option<XmlConfig>,
62) -> Result<String, XmlError>
63where
64    H: Serialize,
65    D: Serialize,
66{
67    let config = config.unwrap_or_default();
68
69    // Determine the namespace based on message type
70    let document_namespace = get_namespace_for_message_type(message_type);
71
72    // Create the envelope
73    let envelope = MxEnvelope::new(header, message, document_namespace);
74
75    // Use custom XML writer for proper formatting
76    if config.pretty_print {
77        format_mx_xml(&envelope, &config)
78    } else {
79        // Use quick-xml for compact output
80        xml_to_string(&envelope).map_err(|e| XmlError::SerializationError(e.to_string()))
81    }
82}
83
84/// Parse complete MX XML with envelope
85pub fn from_mx_xml<H, D>(xml: &str) -> Result<MxEnvelope<H, D>, XmlError>
86where
87    H: for<'de> Deserialize<'de>,
88    D: for<'de> Deserialize<'de>,
89{
90    xml_from_str(xml).map_err(|e| XmlError::DeserializationError(e.to_string()))
91}
92
93/// Format MX XML with proper indentation and structure
94fn format_mx_xml<H, D>(envelope: &MxEnvelope<H, D>, config: &XmlConfig) -> Result<String, XmlError>
95where
96    H: Serialize,
97    D: Serialize,
98{
99    let mut writer = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', config.indent.len());
100
101    // Write XML declaration
102    if config.include_declaration {
103        writer
104            .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
105            .map_err(|e| XmlError::SerializationError(e.to_string()))?;
106    }
107
108    // Start envelope element without namespace (just a wrapper)
109    let envelope_elem = BytesStart::new("Envelope");
110    writer
111        .write_event(Event::Start(envelope_elem))
112        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
113
114    // Write AppHdr with its namespace
115    let mut app_hdr_elem = BytesStart::new("AppHdr");
116    app_hdr_elem.push_attribute(("xmlns", "urn:iso:std:iso:20022:tech:xsd:head.001.001.02"));
117
118    writer
119        .write_event(Event::Start(app_hdr_elem))
120        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
121
122    // Serialize the AppHdr content (without the wrapper element)
123    let app_hdr_xml = xml_to_string(&envelope.app_hdr)
124        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
125
126    // Extract just the inner content (remove XML declaration and AppHdr wrapper tags)
127    let app_hdr_xml = app_hdr_xml
128        .trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
129        .trim();
130
131    // Remove the opening and closing AppHdr tags to get just the content
132    let app_hdr_inner = if app_hdr_xml.starts_with("<AppHdr>") {
133        app_hdr_xml
134            .trim_start_matches("<AppHdr>")
135            .trim_end_matches("</AppHdr>")
136    } else if app_hdr_xml.starts_with("<AppHdr") {
137        // Handle case where AppHdr might have attributes
138        if let Some(pos) = app_hdr_xml.find('>') {
139            let content = &app_hdr_xml[pos + 1..];
140            content.trim_end_matches("</AppHdr>")
141        } else {
142            app_hdr_xml
143        }
144    } else {
145        app_hdr_xml
146    };
147
148    writer
149        .write_event(Event::Text(BytesText::from_escaped(app_hdr_inner)))
150        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
151
152    // Close AppHdr
153    writer
154        .write_event(Event::End(BytesEnd::new("AppHdr")))
155        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
156
157    // Write Document with its namespace
158    let mut doc_elem = BytesStart::new("Document");
159    if let Some(ref xmlns) = envelope.document.xmlns {
160        doc_elem.push_attribute(("xmlns", xmlns.as_str()));
161    }
162
163    writer
164        .write_event(Event::Start(doc_elem))
165        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
166
167    // Write the actual message content
168    let message_xml = xml_to_string(&envelope.document.message)
169        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
170
171    // Remove the XML declaration from the inner serialization if present
172    let message_xml = message_xml
173        .trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
174        .trim();
175
176    writer
177        .write_event(Event::Text(BytesText::from_escaped(message_xml)))
178        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
179
180    // Close Document
181    writer
182        .write_event(Event::End(BytesEnd::new("Document")))
183        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
184
185    // Close Envelope
186    writer
187        .write_event(Event::End(BytesEnd::new("Envelope")))
188        .map_err(|e| XmlError::SerializationError(e.to_string()))?;
189
190    let result = writer.into_inner().into_inner();
191    String::from_utf8(result).map_err(|e| XmlError::SerializationError(e.to_string()))
192}
193
194/// Get the appropriate namespace for a message type
195fn get_namespace_for_message_type(message_type: &str) -> String {
196    let namespace = match message_type {
197        "pacs.008" | "pacs.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08",
198        "pacs.009" | "pacs.009.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.009.001.08",
199        "pacs.003" | "pacs.003.001.08" => "urn:iso:std:iso:20022:tech:xsd:pacs.003.001.08",
200        "pacs.002" | "pacs.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10",
201        "pain.001" | "pain.001.001.09" => "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09",
202        "pain.008" | "pain.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pain.008.001.08",
203        "camt.052" | "camt.052.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.052.001.08",
204        "camt.053" | "camt.053.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08",
205        "camt.054" | "camt.054.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.054.001.08",
206        "camt.056" | "camt.056.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.056.001.08",
207        "camt.057" | "camt.057.001.06" => "urn:iso:std:iso:20022:tech:xsd:camt.057.001.06",
208        "camt.060" | "camt.060.001.05" => "urn:iso:std:iso:20022:tech:xsd:camt.060.001.05",
209        "camt.027" | "camt.027.001.07" => "urn:iso:std:iso:20022:tech:xsd:camt.027.001.07",
210        "camt.029" | "camt.029.001.09" => "urn:iso:std:iso:20022:tech:xsd:camt.029.001.09",
211        _ => {
212            return format!("urn:iso:std:iso:20022:tech:xsd:{message_type}");
213        }
214    };
215    namespace.to_string()
216}
217
218/// Helper function to create XML for a specific message type
219pub fn create_pacs008_xml<D: Serialize>(
220    message: D,
221    from_bic: String,
222    to_bic: String,
223    business_msg_id: String,
224) -> Result<String, XmlError> {
225    use crate::header::bah_pacs_008_001_08::{
226        BranchAndFinancialInstitutionIdentification62, BusinessApplicationHeaderV02,
227        FinancialInstitutionIdentification182, Party44Choice1,
228    };
229
230    let header = BusinessApplicationHeaderV02 {
231        char_set: None,
232        fr: Party44Choice1 {
233            fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
234                fin_instn_id: FinancialInstitutionIdentification182 {
235                    bicfi: from_bic,
236                    clr_sys_mmb_id: None,
237                    lei: None,
238                },
239            }),
240        },
241        to: Party44Choice1 {
242            fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
243                fin_instn_id: FinancialInstitutionIdentification182 {
244                    bicfi: to_bic,
245                    clr_sys_mmb_id: None,
246                    lei: None,
247                },
248            }),
249        },
250        biz_msg_idr: business_msg_id,
251        msg_def_idr: "pacs.008.001.08".to_string(),
252        biz_svc: "swift.ug".to_string(),
253        mkt_prctc: None,
254        cre_dt: chrono::Utc::now()
255            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
256            .to_string(),
257        cpy_dplct: None,
258        pssbl_dplct: None,
259        prty: None,
260        rltd: None,
261    };
262
263    to_mx_xml(message, header, "pacs.008", None)
264}
265
266/// Helper function to create XML for pain.001 message
267pub fn create_pain001_xml<D: Serialize>(
268    message: D,
269    from_bic: String,
270    to_bic: String,
271    business_msg_id: String,
272) -> Result<String, XmlError> {
273    use crate::header::bah_pain_001_001_09::{
274        BranchAndFinancialInstitutionIdentification64, BusinessApplicationHeaderV02,
275        FinancialInstitutionIdentification183, Party44Choice1,
276    };
277
278    let header = BusinessApplicationHeaderV02 {
279        char_set: None,
280        fr: Party44Choice1 {
281            fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
282                fin_instn_id: FinancialInstitutionIdentification183 {
283                    bicfi: from_bic,
284                    clr_sys_mmb_id: None,
285                    lei: None,
286                },
287            }),
288        },
289        to: Party44Choice1 {
290            fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
291                fin_instn_id: FinancialInstitutionIdentification183 {
292                    bicfi: to_bic,
293                    clr_sys_mmb_id: None,
294                    lei: None,
295                },
296            }),
297        },
298        biz_msg_idr: business_msg_id,
299        msg_def_idr: "pain.001.001.09".to_string(),
300        biz_svc: "swift.ug".to_string(),
301        mkt_prctc: None,
302        cre_dt: chrono::Utc::now()
303            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
304            .to_string(),
305        cpy_dplct: None,
306        pssbl_dplct: None,
307        prty: None,
308        rltd: None,
309    };
310
311    to_mx_xml(message, header, "pain.001", None)
312}
313
314/// Helper function to create XML for camt.053 message
315pub fn create_camt053_xml<D: Serialize>(
316    message: D,
317    from_bic: String,
318    to_bic: String,
319    business_msg_id: String,
320) -> Result<String, XmlError> {
321    use crate::header::bah_camt_053_001_08::{
322        BranchAndFinancialInstitutionIdentification63, BusinessApplicationHeaderV02,
323        FinancialInstitutionIdentification182, Party44Choice1,
324    };
325
326    let header = BusinessApplicationHeaderV02 {
327        char_set: None,
328        fr: Party44Choice1 {
329            fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
330                fin_instn_id: FinancialInstitutionIdentification182 {
331                    bicfi: from_bic,
332                    clr_sys_mmb_id: None,
333                    lei: None,
334                },
335            }),
336        },
337        to: Party44Choice1 {
338            fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
339                fin_instn_id: FinancialInstitutionIdentification182 {
340                    bicfi: to_bic,
341                    clr_sys_mmb_id: None,
342                    lei: None,
343                },
344            }),
345        },
346        biz_msg_idr: business_msg_id,
347        msg_def_idr: "camt.053.001.08".to_string(),
348        biz_svc: "swift.ug".to_string(),
349        mkt_prctc: None,
350        cre_dt: chrono::Utc::now()
351            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
352            .to_string(),
353        cpy_dplct: None,
354        pssbl_dplct: None,
355        prty: None,
356        rltd: None,
357    };
358
359    to_mx_xml(message, header, "camt.053", None)
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_namespace_lookup() {
368        assert_eq!(
369            get_namespace_for_message_type("pacs.008"),
370            "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08"
371        );
372        assert_eq!(
373            get_namespace_for_message_type("pain.001"),
374            "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
375        );
376        assert_eq!(
377            get_namespace_for_message_type("camt.053"),
378            "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"
379        );
380    }
381
382    #[test]
383    fn test_xml_config_default() {
384        let config = XmlConfig::default();
385        assert!(config.include_declaration);
386        assert!(config.pretty_print);
387        assert_eq!(config.indent, "  ");
388        assert!(!config.include_schema_location);
389    }
390}