mx_message/
xml.rs

1// XML Serialization and Deserialization utilities for MX Messages
2// This module provides utilities for converting between XML and JSON for ISO20022 messages
3// Used by the plugin system for validation and parsing operations
4
5use crate::error::MxError;
6use crate::message_registry;
7use quick_xml::de::from_str as xml_from_str;
8
9// Re-export MxError as XmlError for backward compatibility
10pub type XmlError = MxError;
11
12/// Deserialize complete MX XML envelope (with AppHdr) to typed structs for validation
13/// This validates both the header and document structure
14/// Used by validation plugin for envelope validation
15pub fn from_mx_xml_envelope_str(xml: &str, message_type: &str) -> Result<(), XmlError> {
16    use crate::mx_envelope::MxMessage;
17
18    // Check if XML contains AppHdr (full envelope) or just Document
19    let has_envelope = xml.contains("<AppHdr") || xml.contains("<Envelope");
20
21    if !has_envelope {
22        // No envelope, validate using MxMessage which handles Document-only
23        MxMessage::from_xml(xml).map_err(|e| {
24            MxError::XmlDeserialization(format!(
25                "Failed to validate Document (message_type={}): {}",
26                message_type, e
27            ))
28        })?;
29        return Ok(());
30    }
31
32    // Parse as full envelope with AppHdr - validate by attempting deserialization
33    // All message types use the same BusinessApplicationHeaderV02
34    xml_from_str::<MxMessage>(xml).map_err(|e| {
35        MxError::XmlDeserialization(format!("Failed to parse {} envelope: {}", message_type, e))
36    })?;
37
38    Ok(())
39}
40
41/// Helper function to get the Document element name for a message type
42/// Used internally by xml_to_json_via_document
43fn get_document_element_name(message_type: &str) -> &'static str {
44    message_registry::message_type_to_element(message_type).unwrap_or("Unknown")
45}
46
47/// Macro to reduce XML to JSON conversion boilerplate
48macro_rules! xml_to_json_doc {
49    ($xml:expr, $path:path, $msg_type:expr) => {{
50        let doc = xml_from_str::<$path>($xml).map_err(|e| {
51            MxError::XmlDeserialization(format!("Failed to parse {}: {}", $msg_type, e))
52        })?;
53        serde_json::to_value(&doc).map_err(|e| MxError::XmlSerialization(e.to_string()))?
54    }};
55}
56
57/// Parse XML to JSON using typed ISO20022 document structures
58/// This is the correct way to parse XML that preserves arrays and complex structures
59/// Used by parse plugin for Document-only XML parsing
60pub fn xml_to_json_via_document(
61    xml: &str,
62    message_type: &str,
63) -> Result<serde_json::Value, XmlError> {
64    use crate::document::*;
65    use serde_json::json;
66
67    // Extract inner XML (strip Document wrapper and XML declaration)
68    let inner_xml = xml
69        .trim()
70        .trim_start_matches("<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
71        .trim();
72
73    // Find the Document wrapper and extract inner content
74    let inner_xml = if let Some(start_idx) = inner_xml.find("<Document") {
75        if let Some(end_bracket) = inner_xml[start_idx..].find('>') {
76            let content_start = start_idx + end_bracket + 1;
77            if let Some(end_idx) = inner_xml.rfind("</Document>") {
78                &inner_xml[content_start..end_idx]
79            } else {
80                inner_xml
81            }
82        } else {
83            inner_xml
84        }
85    } else {
86        inner_xml
87    };
88
89    let inner_xml = inner_xml.trim();
90
91    // Deserialize XML to typed struct, then serialize to JSON
92    let json_value = match message_type {
93        "pacs.008" => xml_to_json_doc!(
94            inner_xml,
95            pacs_008_001_08::FIToFICustomerCreditTransferV08,
96            "pacs.008"
97        ),
98        "pacs.009" => xml_to_json_doc!(
99            inner_xml,
100            pacs_009_001_08::FinancialInstitutionCreditTransferV08,
101            "pacs.009"
102        ),
103        "pacs.003" => xml_to_json_doc!(
104            inner_xml,
105            pacs_003_001_08::FIToFICustomerDirectDebitV08,
106            "pacs.003"
107        ),
108        "pacs.004" => xml_to_json_doc!(inner_xml, pacs_004_001_09::PaymentReturnV09, "pacs.004"),
109        "pacs.010" => xml_to_json_doc!(
110            inner_xml,
111            pacs_010_001_03::FinancialInstitutionDirectDebitV03,
112            "pacs.010"
113        ),
114        "pacs.002" => xml_to_json_doc!(
115            inner_xml,
116            pacs_002_001_10::FIToFIPaymentStatusReportV10,
117            "pacs.002"
118        ),
119        "pain.001" => xml_to_json_doc!(
120            inner_xml,
121            pain_001_001_09::CustomerCreditTransferInitiationV09,
122            "pain.001"
123        ),
124        "pain.008" => xml_to_json_doc!(
125            inner_xml,
126            pain_008_001_08::CustomerDirectDebitInitiationV08,
127            "pain.008"
128        ),
129        "camt.025" => xml_to_json_doc!(inner_xml, camt_025_001_08::ReceiptV08, "camt.025"),
130        "camt.029" => xml_to_json_doc!(
131            inner_xml,
132            camt_029_001_09::ResolutionOfInvestigationV09,
133            "camt.029"
134        ),
135        "camt.052" => xml_to_json_doc!(
136            inner_xml,
137            camt_052_001_08::BankToCustomerAccountReportV08,
138            "camt.052"
139        ),
140        "camt.053" => xml_to_json_doc!(
141            inner_xml,
142            camt_053_001_08::BankToCustomerStatementV08,
143            "camt.053"
144        ),
145        "camt.054" => xml_to_json_doc!(
146            inner_xml,
147            camt_054_001_08::BankToCustomerDebitCreditNotificationV08,
148            "camt.054"
149        ),
150        "camt.056" => xml_to_json_doc!(
151            inner_xml,
152            camt_056_001_08::FIToFIPaymentCancellationRequestV08,
153            "camt.056"
154        ),
155        "camt.057" => xml_to_json_doc!(
156            inner_xml,
157            camt_057_001_06::NotificationToReceiveV06,
158            "camt.057"
159        ),
160        "camt.060" => xml_to_json_doc!(
161            inner_xml,
162            camt_060_001_05::AccountReportingRequestV05,
163            "camt.060"
164        ),
165        "camt.107" => xml_to_json_doc!(
166            inner_xml,
167            camt_107_001_01::ChequePresentmentNotificationV01,
168            "camt.107"
169        ),
170        "camt.108" => xml_to_json_doc!(
171            inner_xml,
172            camt_108_001_01::ChequeCancellationOrStopRequestV01,
173            "camt.108"
174        ),
175        "camt.109" => xml_to_json_doc!(
176            inner_xml,
177            camt_109_001_01::ChequeCancellationOrStopReportV01,
178            "camt.109"
179        ),
180        "admi.024" => xml_to_json_doc!(
181            inner_xml,
182            admi_024_001_01::NotificationOfCorrespondenceV01,
183            "admi.024"
184        ),
185        _ => {
186            return Err(MxError::XmlDeserialization(format!(
187                "Unsupported message type: {}",
188                message_type
189            )));
190        }
191    };
192
193    // Wrap in Document structure with correct element name
194    let element_name = get_document_element_name(message_type);
195    Ok(json!({
196        "Document": {
197            element_name: json_value
198        }
199    }))
200}
201
202/// Helper function to parse XML to JSON Value (for dataflow plugins)
203/// This function parses XML and converts it to a JSON structure
204///
205/// Note: This is a simplified XML parser for plugin use.
206/// It converts XML elements to JSON objects preserving the structure.
207///
208/// Used by mx_envelope for fallback parsing and validate plugin for legacy validation.
209pub fn from_mx_xml_to_json(xml: &str) -> Result<serde_json::Value, XmlError> {
210    // Use quick_xml to deserialize directly to a serde_json::Value
211    // This is simpler than manual parsing and handles the XML-to-JSON conversion
212    let value: serde_json::Value = quick_xml::de::from_str(xml)
213        .map_err(|e| MxError::XmlDeserialization(format!("XML parsing error: {}", e)))?;
214
215    Ok(value)
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn test_get_document_element_name() {
224        assert_eq!(get_document_element_name("pacs.008"), "FIToFICstmrCdtTrf");
225        assert_eq!(get_document_element_name("camt.053"), "BkToCstmrStmt");
226        assert_eq!(get_document_element_name("pain.001"), "CstmrCdtTrfInitn");
227    }
228}