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.004" | "pacs.004.001.09" => "urn:iso:std:iso:20022:tech:xsd:pacs.004.001.09",
201        "pacs.002" | "pacs.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pacs.002.001.10",
202        "pain.001" | "pain.001.001.09" => "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09",
203        "pain.002" | "pain.002.001.10" => "urn:iso:std:iso:20022:tech:xsd:pain.002.001.10",
204        "pain.008" | "pain.008.001.08" => "urn:iso:std:iso:20022:tech:xsd:pain.008.001.08",
205        "camt.025" | "camt.025.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.025.001.08",
206        "camt.052" | "camt.052.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.052.001.08",
207        "camt.053" | "camt.053.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08",
208        "camt.054" | "camt.054.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.054.001.08",
209        "camt.056" | "camt.056.001.08" => "urn:iso:std:iso:20022:tech:xsd:camt.056.001.08",
210        "camt.057" | "camt.057.001.06" => "urn:iso:std:iso:20022:tech:xsd:camt.057.001.06",
211        "camt.060" | "camt.060.001.05" => "urn:iso:std:iso:20022:tech:xsd:camt.060.001.05",
212        "camt.027" | "camt.027.001.07" => "urn:iso:std:iso:20022:tech:xsd:camt.027.001.07",
213        "camt.029" | "camt.029.001.09" => "urn:iso:std:iso:20022:tech:xsd:camt.029.001.09",
214        "camt.107" | "camt.107.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.107.001.01",
215        "camt.108" | "camt.108.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.108.001.01",
216        "camt.109" | "camt.109.001.01" => "urn:iso:std:iso:20022:tech:xsd:camt.109.001.01",
217        _ => {
218            return format!("urn:iso:std:iso:20022:tech:xsd:{message_type}");
219        }
220    };
221    namespace.to_string()
222}
223
224/// Helper function to create XML for a specific message type
225pub fn create_pacs008_xml<D: Serialize>(
226    message: D,
227    from_bic: String,
228    to_bic: String,
229    business_msg_id: String,
230) -> Result<String, XmlError> {
231    use crate::header::bah_pacs_008_001_08::{
232        BranchAndFinancialInstitutionIdentification62, BusinessApplicationHeaderV02,
233        FinancialInstitutionIdentification182, Party44Choice1,
234    };
235
236    let header = BusinessApplicationHeaderV02 {
237        char_set: None,
238        fr: Party44Choice1 {
239            fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
240                fin_instn_id: FinancialInstitutionIdentification182 {
241                    bicfi: from_bic,
242                    clr_sys_mmb_id: None,
243                    lei: None,
244                },
245            }),
246        },
247        to: Party44Choice1 {
248            fi_id: Some(BranchAndFinancialInstitutionIdentification62 {
249                fin_instn_id: FinancialInstitutionIdentification182 {
250                    bicfi: to_bic,
251                    clr_sys_mmb_id: None,
252                    lei: None,
253                },
254            }),
255        },
256        biz_msg_idr: business_msg_id,
257        msg_def_idr: "pacs.008.001.08".to_string(),
258        biz_svc: "swift.ug".to_string(),
259        mkt_prctc: None,
260        cre_dt: chrono::Utc::now()
261            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
262            .to_string(),
263        cpy_dplct: None,
264        pssbl_dplct: None,
265        prty: None,
266        rltd: None,
267    };
268
269    to_mx_xml(message, header, "pacs.008", None)
270}
271
272/// Helper function to create XML for pain.001 message
273pub fn create_pain001_xml<D: Serialize>(
274    message: D,
275    from_bic: String,
276    to_bic: String,
277    business_msg_id: String,
278) -> Result<String, XmlError> {
279    use crate::header::bah_pain_001_001_09::{
280        BranchAndFinancialInstitutionIdentification64, BusinessApplicationHeaderV02,
281        FinancialInstitutionIdentification183, Party44Choice1,
282    };
283
284    let header = BusinessApplicationHeaderV02 {
285        char_set: None,
286        fr: Party44Choice1 {
287            fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
288                fin_instn_id: FinancialInstitutionIdentification183 {
289                    bicfi: from_bic,
290                    clr_sys_mmb_id: None,
291                    lei: None,
292                },
293            }),
294        },
295        to: Party44Choice1 {
296            fi_id: Some(BranchAndFinancialInstitutionIdentification64 {
297                fin_instn_id: FinancialInstitutionIdentification183 {
298                    bicfi: to_bic,
299                    clr_sys_mmb_id: None,
300                    lei: None,
301                },
302            }),
303        },
304        biz_msg_idr: business_msg_id,
305        msg_def_idr: "pain.001.001.09".to_string(),
306        biz_svc: "swift.ug".to_string(),
307        mkt_prctc: None,
308        cre_dt: chrono::Utc::now()
309            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
310            .to_string(),
311        cpy_dplct: None,
312        pssbl_dplct: None,
313        prty: None,
314        rltd: None,
315    };
316
317    to_mx_xml(message, header, "pain.001", None)
318}
319
320/// Helper function to create XML for camt.053 message
321pub fn create_camt053_xml<D: Serialize>(
322    message: D,
323    from_bic: String,
324    to_bic: String,
325    business_msg_id: String,
326) -> Result<String, XmlError> {
327    use crate::header::bah_camt_053_001_08::{
328        BranchAndFinancialInstitutionIdentification63, BusinessApplicationHeaderV02,
329        FinancialInstitutionIdentification182, Party44Choice1,
330    };
331
332    let header = BusinessApplicationHeaderV02 {
333        char_set: None,
334        fr: Party44Choice1 {
335            fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
336                fin_instn_id: FinancialInstitutionIdentification182 {
337                    bicfi: from_bic,
338                    clr_sys_mmb_id: None,
339                    lei: None,
340                },
341            }),
342        },
343        to: Party44Choice1 {
344            fi_id: Some(BranchAndFinancialInstitutionIdentification63 {
345                fin_instn_id: FinancialInstitutionIdentification182 {
346                    bicfi: to_bic,
347                    clr_sys_mmb_id: None,
348                    lei: None,
349                },
350            }),
351        },
352        biz_msg_idr: business_msg_id,
353        msg_def_idr: "camt.053.001.08".to_string(),
354        biz_svc: "swift.ug".to_string(),
355        mkt_prctc: None,
356        cre_dt: chrono::Utc::now()
357            .format("%Y-%m-%dT%H:%M:%S%.3fZ")
358            .to_string(),
359        cpy_dplct: None,
360        pssbl_dplct: None,
361        prty: None,
362        rltd: None,
363    };
364
365    to_mx_xml(message, header, "camt.053", None)
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_namespace_lookup() {
374        assert_eq!(
375            get_namespace_for_message_type("pacs.008"),
376            "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08"
377        );
378        assert_eq!(
379            get_namespace_for_message_type("pain.001"),
380            "urn:iso:std:iso:20022:tech:xsd:pain.001.001.09"
381        );
382        assert_eq!(
383            get_namespace_for_message_type("camt.053"),
384            "urn:iso:std:iso:20022:tech:xsd:camt.053.001.08"
385        );
386    }
387
388    #[test]
389    fn test_xml_config_default() {
390        let config = XmlConfig::default();
391        assert!(config.include_declaration);
392        assert!(config.pretty_print);
393        assert_eq!(config.indent, "  ");
394        assert!(!config.include_schema_location);
395    }
396}