mx_message/
mx_envelope.rs

1// MX Message Envelope Structure for ISO 20022 compliant XML generation
2
3use serde::{Deserialize, Serialize};
4
5// Re-export AppHdr for convenience
6use crate::error::MxError;
7pub use crate::header::AppHdr;
8use crate::message_registry;
9
10/// Document enum - represents the Document element in MX messages
11/// Each variant uses serde rename to match the XML element name
12/// Box wrappers are used to prevent stack overflow from large variants
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub enum Document {
15    // PACS - Payment Clearing and Settlement
16    #[serde(rename = "FIToFICstmrCdtTrf")]
17    Pacs008(Box<crate::document::pacs_008_001_08::FIToFICustomerCreditTransferV08>),
18
19    #[serde(rename = "FIToFIPmtStsRpt")]
20    Pacs002(Box<crate::document::pacs_002_001_10::FIToFIPaymentStatusReportV10>),
21
22    #[serde(rename = "FIToFICstmrDrctDbt")]
23    Pacs003(Box<crate::document::pacs_003_001_08::FIToFICustomerDirectDebitV08>),
24
25    #[serde(rename = "PmtRtr")]
26    Pacs004(Box<crate::document::pacs_004_001_09::PaymentReturnV09>),
27
28    #[serde(rename = "FICdtTrf")]
29    Pacs009(Box<crate::document::pacs_009_001_08::FinancialInstitutionCreditTransferV08>),
30
31    #[serde(rename = "FIDrctDbt")]
32    Pacs010(Box<crate::document::pacs_010_001_03::FinancialInstitutionDirectDebitV03>),
33
34    // PAIN - Payment Initiation
35    #[serde(rename = "CstmrCdtTrfInitn")]
36    Pain001(Box<crate::document::pain_001_001_09::CustomerCreditTransferInitiationV09>),
37
38    #[serde(rename = "CstmrPmtStsRpt")]
39    Pain002(Box<crate::document::pain_002_001_10::CustomerPaymentStatusReportV10>),
40
41    #[serde(rename = "CstmrDrctDbtInitn")]
42    Pain008(Box<crate::document::pain_008_001_08::CustomerDirectDebitInitiationV08>),
43
44    // CAMT - Cash Management
45    #[serde(rename = "Rcpt")]
46    Camt025(Box<crate::document::camt_025_001_08::ReceiptV08>),
47
48    #[serde(rename = "RsltnOfInvstgtn")]
49    Camt029(Box<crate::document::camt_029_001_09::ResolutionOfInvestigationV09>),
50
51    #[serde(rename = "BkToCstmrAcctRpt")]
52    Camt052(Box<crate::document::camt_052_001_08::BankToCustomerAccountReportV08>),
53
54    #[serde(rename = "BkToCstmrStmt")]
55    Camt053(Box<crate::document::camt_053_001_08::BankToCustomerStatementV08>),
56
57    #[serde(rename = "BkToCstmrDbtCdtNtfctn")]
58    Camt054(Box<crate::document::camt_054_001_08::BankToCustomerDebitCreditNotificationV08>),
59
60    #[serde(rename = "FIToFIPmtCxlReq")]
61    Camt056(Box<crate::document::camt_056_001_08::FIToFIPaymentCancellationRequestV08>),
62
63    #[serde(rename = "NtfctnToRcv")]
64    Camt057(Box<crate::document::camt_057_001_06::NotificationToReceiveV06>),
65
66    #[serde(rename = "AcctRptgReq")]
67    Camt060(Box<crate::document::camt_060_001_05::AccountReportingRequestV05>),
68
69    #[serde(rename = "ChqPresntmntNtfctn")]
70    Camt107(Box<crate::document::camt_107_001_01::ChequePresentmentNotificationV01>),
71
72    #[serde(rename = "ChqCxlOrStopReq")]
73    Camt108(Box<crate::document::camt_108_001_01::ChequeCancellationOrStopRequestV01>),
74
75    #[serde(rename = "ChqCxlOrStopRpt")]
76    Camt109(Box<crate::document::camt_109_001_01::ChequeCancellationOrStopReportV01>),
77
78    // ADMI - Administration
79    #[serde(rename = "NtfctnOfCrrspndnc")]
80    Admi024(Box<crate::document::admi_024_001_01::NotificationOfCorrespondenceV01>),
81}
82
83impl Document {
84    /// Get the namespace for this document based on its type
85    pub fn namespace(&self) -> String {
86        let msg_type = match self {
87            Document::Pacs008(_) => "pacs.008",
88            Document::Pacs009(_) => "pacs.009",
89            Document::Pacs003(_) => "pacs.003",
90            Document::Pacs004(_) => "pacs.004",
91            Document::Pacs002(_) => "pacs.002",
92            Document::Pacs010(_) => "pacs.010",
93            Document::Pain001(_) => "pain.001",
94            Document::Pain002(_) => "pain.002",
95            Document::Pain008(_) => "pain.008",
96            Document::Camt025(_) => "camt.025",
97            Document::Camt029(_) => "camt.029",
98            Document::Camt052(_) => "camt.052",
99            Document::Camt053(_) => "camt.053",
100            Document::Camt054(_) => "camt.054",
101            Document::Camt056(_) => "camt.056",
102            Document::Camt057(_) => "camt.057",
103            Document::Camt060(_) => "camt.060",
104            Document::Camt107(_) => "camt.107",
105            Document::Camt108(_) => "camt.108",
106            Document::Camt109(_) => "camt.109",
107            Document::Admi024(_) => "admi.024",
108        };
109        message_registry::get_namespace(msg_type)
110    }
111}
112
113/// Complete MX message containing Business Application Header and Document
114/// This is the unified structure for all ISO20022 message types
115/// The message type is determined from the AppHdr.MsgDefIdr field
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117#[serde(rename = "Envelope")]
118pub struct MxMessage {
119    /// XML namespace declarations
120    #[serde(rename = "@xmlns", skip_serializing_if = "Option::is_none")]
121    pub xmlns: Option<String>,
122
123    #[serde(rename = "@xmlns:xsi", skip_serializing_if = "Option::is_none")]
124    pub xmlns_xsi: Option<String>,
125
126    /// Business Application Header
127    #[serde(rename = "AppHdr")]
128    pub app_hdr: crate::header::AppHdr,
129
130    /// Document containing the actual message
131    #[serde(rename = "Document")]
132    pub document: Document,
133}
134
135impl MxMessage {
136    /// Create a new MX message with default namespaces
137    pub fn new(app_hdr: crate::header::AppHdr, document: Document) -> Self {
138        Self {
139            xmlns: Some("urn:iso:std:iso:20022:tech:xsd:head.001.001.02".to_string()),
140            xmlns_xsi: Some("http://www.w3.org/2001/XMLSchema-instance".to_string()),
141            app_hdr,
142            document,
143        }
144    }
145}
146
147/// Get the appropriate namespace for a message type
148/// Delegates to message_registry module
149pub fn get_namespace_for_message_type(message_type: &str) -> String {
150    message_registry::get_namespace(message_type)
151}
152
153/// Get the short form of message type (e.g., "pacs.008")
154/// Delegates to message_registry module
155pub fn normalize_message_type(message_type: &str) -> String {
156    message_registry::normalize_message_type(message_type)
157}
158
159/// Macro to reduce serialization boilerplate
160macro_rules! serialize_doc {
161    ($doc:expr, $rust_type:expr, $xml_elem:expr, $msg_type:expr) => {
162        MxMessage::serialize_with_rename($doc.as_ref(), $rust_type, $xml_elem, $msg_type)
163    };
164}
165
166/// Macro to reduce deserialization boilerplate
167macro_rules! deserialize_doc {
168    ($xml:expr, $path:path, $variant:ident, $msg_type:expr) => {{
169        let doc = quick_xml::de::from_str::<$path>($xml).map_err(|e| {
170            MxError::XmlDeserialization(format!("Failed to parse {}: {}", $msg_type, e))
171        })?;
172        Ok(Document::$variant(Box::new(doc)))
173    }};
174}
175
176impl MxMessage {
177    /// Get the message type identifier from AppHdr (e.g., "pacs.008.001.08")
178    pub fn message_type(&self) -> Result<&str, MxError> {
179        Ok(&self.app_hdr.msg_def_idr)
180    }
181
182    /// Get the namespace for this message
183    pub fn namespace(&self) -> Result<String, MxError> {
184        Ok(get_namespace_for_message_type(self.message_type()?))
185    }
186
187    /// Helper function to serialize a document with struct name replacement
188    fn serialize_with_rename<T: Serialize>(
189        value: &T,
190        rust_type: &str,
191        xml_element: &str,
192        msg_type: &str,
193    ) -> Result<String, MxError> {
194        let xml = quick_xml::se::to_string(value).map_err(|e| {
195            MxError::XmlSerialization(format!("Failed to serialize {}: {}", msg_type, e))
196        })?;
197        Ok(xml
198            .replace(&format!("<{}>", rust_type), &format!("<{}>", xml_element))
199            .replace(&format!("</{}>", rust_type), &format!("</{}>", xml_element)))
200    }
201
202    /// Serialize to XML string
203    pub fn to_xml(&self) -> Result<String, MxError> {
204        // Custom serialization to handle enum variants
205        // Serialize AppHdr
206        let app_hdr_xml = quick_xml::se::to_string(&self.app_hdr)
207            .map_err(|e| MxError::XmlSerialization(format!("Failed to serialize AppHdr: {}", e)))?;
208
209        // AppHdr is serialized directly without a wrapper tag, use it as-is
210        let app_hdr_inner = app_hdr_xml;
211
212        // Serialize Document based on its variant
213        let doc_xml = self.serialize_document()?;
214
215        // Build complete envelope
216        // Note: quick-xml adds struct name as wrapper, so app_hdr_inner already contains <BusinessApplicationHeaderV02>
217        // We need to rename it to <AppHdr>
218        let app_hdr_wrapped = app_hdr_inner
219            .replace("<BusinessApplicationHeaderV02>", "<AppHdr>")
220            .replace("</BusinessApplicationHeaderV02>", "</AppHdr>");
221
222        let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
223        xml.push_str("<Envelope>");
224        xml.push_str(&app_hdr_wrapped);
225        xml.push_str("<Document>");
226        xml.push_str(&doc_xml);
227        xml.push_str("</Document>");
228        xml.push_str("</Envelope>");
229
230        Ok(xml)
231    }
232
233    /// Serialize document based on its variant
234    fn serialize_document(&self) -> Result<String, MxError> {
235        match &self.document {
236            Document::Pacs008(doc) => serialize_doc!(
237                doc,
238                "FIToFICustomerCreditTransferV08",
239                "FIToFICstmrCdtTrf",
240                "pacs.008"
241            ),
242            Document::Pacs002(doc) => serialize_doc!(
243                doc,
244                "FIToFIPaymentStatusReportV10",
245                "FIToFIPmtStsRpt",
246                "pacs.002"
247            ),
248            Document::Pacs003(doc) => serialize_doc!(
249                doc,
250                "FIToFICustomerDirectDebitV08",
251                "FIToFICstmrDrctDbt",
252                "pacs.003"
253            ),
254            Document::Pacs004(doc) => serialize_doc!(doc, "PaymentReturnV09", "PmtRtr", "pacs.004"),
255            Document::Pacs009(doc) => serialize_doc!(
256                doc,
257                "FinancialInstitutionCreditTransferV08",
258                "FICdtTrf",
259                "pacs.009"
260            ),
261            Document::Pacs010(doc) => serialize_doc!(
262                doc,
263                "FinancialInstitutionDirectDebitV03",
264                "FIDrctDbt",
265                "pacs.010"
266            ),
267            Document::Pain001(doc) => serialize_doc!(
268                doc,
269                "CustomerCreditTransferInitiationV09",
270                "CstmrCdtTrfInitn",
271                "pain.001"
272            ),
273            Document::Pain002(doc) => serialize_doc!(
274                doc,
275                "CustomerPaymentStatusReportV10",
276                "CstmrPmtStsRpt",
277                "pain.002"
278            ),
279            Document::Pain008(doc) => serialize_doc!(
280                doc,
281                "CustomerDirectDebitInitiationV08",
282                "CstmrDrctDbtInitn",
283                "pain.008"
284            ),
285            Document::Camt025(doc) => serialize_doc!(doc, "ReceiptV08", "Rcpt", "camt.025"),
286            Document::Camt029(doc) => serialize_doc!(
287                doc,
288                "ResolutionOfInvestigationV09",
289                "RsltnOfInvstgtn",
290                "camt.029"
291            ),
292            Document::Camt052(doc) => serialize_doc!(
293                doc,
294                "BankToCustomerAccountReportV08",
295                "BkToCstmrAcctRpt",
296                "camt.052"
297            ),
298            Document::Camt053(doc) => serialize_doc!(
299                doc,
300                "BankToCustomerStatementV08",
301                "BkToCstmrStmt",
302                "camt.053"
303            ),
304            Document::Camt054(doc) => serialize_doc!(
305                doc,
306                "BankToCustomerDebitCreditNotificationV08",
307                "BkToCstmrDbtCdtNtfctn",
308                "camt.054"
309            ),
310            Document::Camt056(doc) => serialize_doc!(
311                doc,
312                "FIToFIPaymentCancellationRequestV08",
313                "FIToFIPmtCxlReq",
314                "camt.056"
315            ),
316            Document::Camt057(doc) => {
317                serialize_doc!(doc, "NotificationToReceiveV06", "NtfctnToRcv", "camt.057")
318            }
319            Document::Camt060(doc) => {
320                serialize_doc!(doc, "AccountReportingRequestV05", "AcctRptgReq", "camt.060")
321            }
322            Document::Camt107(doc) => serialize_doc!(
323                doc,
324                "ChequePresentmentNotificationV01",
325                "ChqPresntmntNtfctn",
326                "camt.107"
327            ),
328            Document::Camt108(doc) => serialize_doc!(
329                doc,
330                "ChequeCancellationOrStopRequestV01",
331                "ChqCxlOrStopReq",
332                "camt.108"
333            ),
334            Document::Camt109(doc) => serialize_doc!(
335                doc,
336                "ChequeCancellationOrStopReportV01",
337                "ChqCxlOrStopRpt",
338                "camt.109"
339            ),
340            Document::Admi024(doc) => serialize_doc!(
341                doc,
342                "NotificationOfCorrespondenceV01",
343                "NtfctnOfCrrspndnc",
344                "admi.024"
345            ),
346        }
347    }
348
349    /// Serialize to JSON string
350    pub fn to_json(&self) -> Result<String, MxError> {
351        serde_json::to_string_pretty(self).map_err(|e| MxError::XmlSerialization(e.to_string()))
352    }
353
354    /// Deserialize from XML string using quick-xml with custom enum handling
355    pub fn from_xml(xml: &str) -> Result<Self, MxError> {
356        // Check if XML contains full envelope or just Document
357        let has_envelope = xml.contains("<AppHdr") || xml.contains("<Envelope");
358
359        if has_envelope {
360            Self::from_xml_with_envelope(xml)
361        } else {
362            Self::from_xml_document_only(xml)
363        }
364    }
365
366    /// Deserialize XML with full envelope (AppHdr + Document)
367    fn from_xml_with_envelope(xml: &str) -> Result<Self, MxError> {
368        // Extract AppHdr section
369        let app_hdr_xml = Self::extract_section(xml, "AppHdr")
370            .ok_or_else(|| MxError::XmlDeserialization("AppHdr not found in XML".to_string()))?;
371
372        // Deserialize AppHdr using quick-xml
373        let app_hdr: crate::header::AppHdr =
374            quick_xml::de::from_str(&format!("<AppHdr>{}</AppHdr>", app_hdr_xml)).map_err(|e| {
375                MxError::XmlDeserialization(format!("Failed to parse AppHdr: {}", e))
376            })?;
377
378        // Extract Document section
379        let doc_xml = Self::extract_section(xml, "Document")
380            .ok_or_else(|| MxError::XmlDeserialization("Document not found in XML".to_string()))?;
381
382        // Determine document type from the first element inside Document
383        let doc_type = Self::detect_document_type(&doc_xml)?;
384
385        // Deserialize the document based on its type
386        let document = Self::deserialize_document(&doc_xml, &doc_type)?;
387
388        // Extract namespace attributes if present
389        let xmlns = Self::extract_attribute(xml, "xmlns");
390        let xmlns_xsi = Self::extract_attribute(xml, "xmlns:xsi");
391
392        Ok(MxMessage {
393            xmlns,
394            xmlns_xsi,
395            app_hdr,
396            document,
397        })
398    }
399
400    /// Deserialize document-only XML (no envelope)
401    fn from_xml_document_only(_xml: &str) -> Result<Self, MxError> {
402        // For document-only XML, we need to create a minimal AppHdr
403        // This is a fallback case - typically we expect full envelopes
404        Err(MxError::XmlDeserialization(
405            "Document-only XML requires AppHdr information. Use full envelope format.".to_string(),
406        ))
407    }
408
409    /// Extract content between XML tags
410    fn extract_section(xml: &str, tag: &str) -> Option<String> {
411        let start_tag = format!("<{}", tag);
412        let end_tag = format!("</{}>", tag);
413
414        let start_idx = xml.find(&start_tag)?;
415        let content_start = xml[start_idx..].find('>')? + start_idx + 1;
416        let end_idx = xml.find(&end_tag)?;
417
418        if content_start < end_idx {
419            Some(xml[content_start..end_idx].to_string())
420        } else {
421            None
422        }
423    }
424
425    /// Extract XML attribute value
426    fn extract_attribute(xml: &str, attr: &str) -> Option<String> {
427        let pattern = format!("{}=\"", attr);
428        let start_idx = xml.find(&pattern)? + pattern.len();
429        let end_idx = xml[start_idx..].find('"')? + start_idx;
430        Some(xml[start_idx..end_idx].to_string())
431    }
432
433    /// Detect document type from XML content
434    fn detect_document_type(doc_xml: &str) -> Result<String, MxError> {
435        // Find the first element tag after any whitespace
436        let trimmed = doc_xml.trim();
437        if !trimmed.starts_with('<') {
438            return Err(MxError::XmlDeserialization(
439                "Invalid document XML structure".to_string(),
440            ));
441        }
442
443        let end_idx = trimmed[1..]
444            .find(|c: char| c.is_whitespace() || c == '>')
445            .map(|i| i + 1)
446            .ok_or_else(|| {
447                MxError::XmlDeserialization("Could not find document element".to_string())
448            })?;
449
450        let element_name = &trimmed[1..end_idx];
451
452        // Map element name to message type using message registry
453        let message_type =
454            message_registry::element_to_message_type(element_name).ok_or_else(|| {
455                MxError::XmlDeserialization(format!("Unknown document type: {}", element_name))
456            })?;
457
458        Ok(message_type.to_string())
459    }
460
461    /// Deserialize document based on message type
462    fn deserialize_document(doc_xml: &str, message_type: &str) -> Result<Document, MxError> {
463        use crate::document::*;
464
465        match message_type {
466            "pacs.008" => deserialize_doc!(
467                doc_xml,
468                pacs_008_001_08::FIToFICustomerCreditTransferV08,
469                Pacs008,
470                "pacs.008"
471            ),
472            "pacs.002" => deserialize_doc!(
473                doc_xml,
474                pacs_002_001_10::FIToFIPaymentStatusReportV10,
475                Pacs002,
476                "pacs.002"
477            ),
478            "pacs.003" => deserialize_doc!(
479                doc_xml,
480                pacs_003_001_08::FIToFICustomerDirectDebitV08,
481                Pacs003,
482                "pacs.003"
483            ),
484            "pacs.004" => deserialize_doc!(
485                doc_xml,
486                pacs_004_001_09::PaymentReturnV09,
487                Pacs004,
488                "pacs.004"
489            ),
490            "pacs.009" => deserialize_doc!(
491                doc_xml,
492                pacs_009_001_08::FinancialInstitutionCreditTransferV08,
493                Pacs009,
494                "pacs.009"
495            ),
496            "pacs.010" => deserialize_doc!(
497                doc_xml,
498                pacs_010_001_03::FinancialInstitutionDirectDebitV03,
499                Pacs010,
500                "pacs.010"
501            ),
502            "pain.001" => deserialize_doc!(
503                doc_xml,
504                pain_001_001_09::CustomerCreditTransferInitiationV09,
505                Pain001,
506                "pain.001"
507            ),
508            "pain.002" => deserialize_doc!(
509                doc_xml,
510                pain_002_001_10::CustomerPaymentStatusReportV10,
511                Pain002,
512                "pain.002"
513            ),
514            "pain.008" => deserialize_doc!(
515                doc_xml,
516                pain_008_001_08::CustomerDirectDebitInitiationV08,
517                Pain008,
518                "pain.008"
519            ),
520            "camt.025" => {
521                deserialize_doc!(doc_xml, camt_025_001_08::ReceiptV08, Camt025, "camt.025")
522            }
523            "camt.029" => deserialize_doc!(
524                doc_xml,
525                camt_029_001_09::ResolutionOfInvestigationV09,
526                Camt029,
527                "camt.029"
528            ),
529            "camt.052" => deserialize_doc!(
530                doc_xml,
531                camt_052_001_08::BankToCustomerAccountReportV08,
532                Camt052,
533                "camt.052"
534            ),
535            "camt.053" => deserialize_doc!(
536                doc_xml,
537                camt_053_001_08::BankToCustomerStatementV08,
538                Camt053,
539                "camt.053"
540            ),
541            "camt.054" => deserialize_doc!(
542                doc_xml,
543                camt_054_001_08::BankToCustomerDebitCreditNotificationV08,
544                Camt054,
545                "camt.054"
546            ),
547            "camt.056" => deserialize_doc!(
548                doc_xml,
549                camt_056_001_08::FIToFIPaymentCancellationRequestV08,
550                Camt056,
551                "camt.056"
552            ),
553            "camt.057" => deserialize_doc!(
554                doc_xml,
555                camt_057_001_06::NotificationToReceiveV06,
556                Camt057,
557                "camt.057"
558            ),
559            "camt.060" => deserialize_doc!(
560                doc_xml,
561                camt_060_001_05::AccountReportingRequestV05,
562                Camt060,
563                "camt.060"
564            ),
565            "camt.107" => deserialize_doc!(
566                doc_xml,
567                camt_107_001_01::ChequePresentmentNotificationV01,
568                Camt107,
569                "camt.107"
570            ),
571            "camt.108" => deserialize_doc!(
572                doc_xml,
573                camt_108_001_01::ChequeCancellationOrStopRequestV01,
574                Camt108,
575                "camt.108"
576            ),
577            "camt.109" => deserialize_doc!(
578                doc_xml,
579                camt_109_001_01::ChequeCancellationOrStopReportV01,
580                Camt109,
581                "camt.109"
582            ),
583            "admi.024" => deserialize_doc!(
584                doc_xml,
585                admi_024_001_01::NotificationOfCorrespondenceV01,
586                Admi024,
587                "admi.024"
588            ),
589            _ => Err(MxError::XmlDeserialization(format!(
590                "Unsupported message type: {}",
591                message_type
592            ))),
593        }
594    }
595
596    /// Deserialize from JSON string
597    pub fn from_json(json: &str) -> Result<Self, MxError> {
598        let message: MxMessage = serde_json::from_str(json).map_err(|e| {
599            MxError::XmlDeserialization(format!("JSON deserialization failed: {}", e))
600        })?;
601
602        Ok(message)
603    }
604}
605
606/// Extract message type from XML without full deserialization
607pub fn peek_message_type_from_xml(xml: &str) -> Result<String, MxError> {
608    // Simple regex-based extraction for MsgDefIdr
609    use regex::Regex;
610
611    let re = Regex::new(r"<MsgDefIdr>([^<]+)</MsgDefIdr>")
612        .map_err(|e| MxError::XmlDeserialization(format!("Regex error: {}", e)))?;
613
614    if let Some(captures) = re.captures(xml)
615        && let Some(msg_def_idr) = captures.get(1)
616    {
617        return Ok(normalize_message_type(msg_def_idr.as_str()));
618    }
619
620    Err(MxError::XmlDeserialization(
621        "Could not find MsgDefIdr in XML".to_string(),
622    ))
623}
624
625/// Extract message type from JSON without full deserialization
626pub fn peek_message_type_from_json(json: &str) -> Result<String, MxError> {
627    let value: serde_json::Value = serde_json::from_str(json)
628        .map_err(|e| MxError::XmlDeserialization(format!("JSON parsing error: {}", e)))?;
629
630    // Try to extract MsgDefIdr from AppHdr
631    if let Some(msg_def_idr) = value
632        .get("AppHdr")
633        .or_else(|| value.get("Envelope").and_then(|e| e.get("AppHdr")))
634        .and_then(|hdr| hdr.get("MsgDefIdr"))
635        .and_then(|v| v.as_str())
636    {
637        return Ok(normalize_message_type(msg_def_idr));
638    }
639
640    Err(MxError::XmlDeserialization(
641        "Could not find MsgDefIdr in JSON".to_string(),
642    ))
643}