mx_message/plugin/
common.rs

1//! Common utilities for MX message plugin operations
2//!
3//! This module contains shared functionality used across multiple plugin handlers
4//! to reduce code duplication and improve maintainability.
5
6use crate::message_registry;
7use dataflow_rs::engine::error::{DataflowError, Result};
8use serde_json::Value;
9
10/// Map ISO20022 document element names to message types
11///
12/// # Arguments
13/// * `element_name` - The document element name (e.g., "FIToFICstmrCdtTrf")
14///
15/// # Returns
16/// * Ok(message_type) - The corresponding message type (e.g., "pacs.008")
17/// * Err(error) - If the element name is not recognized
18pub fn map_document_element_to_message_type(element_name: &str) -> Result<String> {
19    // Use message registry for standard mappings
20    if let Some(msg_type) = message_registry::element_to_message_type(element_name) {
21        return Ok(msg_type.to_string());
22    }
23
24    // Handle legacy aliases not in registry
25    let message_type = match element_name {
26        "FinInstnCdtTrf" => "pacs.009", // Legacy alias for FICdtTrf
27        "Rct" => "camt.025",            // Typo variant of Rcpt
28        "ClmNonRct" | "ClaimNonReceiptV07" => "camt.027", // Not yet in registry
29        _ => {
30            return Err(DataflowError::Validation(format!(
31                "Unknown document element: {}",
32                element_name
33            )));
34        }
35    };
36    Ok(message_type.to_string())
37}
38
39/// Extract message type from parsed JSON data
40///
41/// This function attempts to extract the message type from various locations
42/// in the parsed JSON structure:
43/// 1. Direct "message_type" field at root level
44/// 2. From Document structure by examining the first key
45///
46/// # Arguments
47/// * `data` - The parsed JSON data
48///
49/// # Returns
50/// * Ok(message_type) - The extracted message type
51/// * Err(error) - If message type cannot be determined
52pub fn extract_message_type(data: &Value) -> Result<String> {
53    // Try to get message_type from root level
54    if let Some(mt) = data.get("message_type").and_then(Value::as_str) {
55        return Ok(mt.to_string());
56    }
57
58    // Try to extract from Document structure
59    if let Some(doc) = data.get("Document") {
60        // Get the first key under Document (e.g., "FIToFICstmrCdtTrf")
61        if let Some(obj) = doc.as_object()
62            && let Some(first_key) = obj.keys().next()
63        {
64            // Map document element name to message type
65            return map_document_element_to_message_type(first_key);
66        }
67    }
68
69    Err(DataflowError::Validation(
70        "Could not determine message type from parsed data".to_string(),
71    ))
72}
73
74/// Extract message type from XML string
75///
76/// This function performs a lightweight parsing of XML to extract the message type
77/// without fully parsing the entire document.
78///
79/// # Arguments
80/// * `xml_str` - The XML string to analyze
81///
82/// # Returns
83/// * Ok(message_type) - The extracted message type
84/// * Err(error) - If message type cannot be determined
85pub fn extract_message_type_from_xml(xml_str: &str) -> Result<String> {
86    let xml_str = xml_str.trim();
87
88    // Find the Document opening tag
89    if let Some(doc_start) = xml_str.find("<Document") {
90        // Find the end of the opening Document tag
91        if let Some(doc_end) = xml_str[doc_start..].find('>') {
92            let after_doc = &xml_str[doc_start + doc_end + 1..];
93
94            // Find the first element after Document
95            if let Some(elem_start) = after_doc.find('<')
96                && after_doc.as_bytes()[elem_start + 1] != b'/'
97            {
98                let elem_name_start = elem_start + 1;
99                // Find the end of the element name (space or >)
100                let elem_name_end = after_doc[elem_name_start..]
101                    .find([' ', '>', '/'])
102                    .map(|i| elem_name_start + i)
103                    .unwrap_or(after_doc.len());
104
105                let element_name = &after_doc[elem_name_start..elem_name_end];
106                // Remove namespace prefix if present
107                let element_name = if let Some(colon_pos) = element_name.rfind(':') {
108                    &element_name[colon_pos + 1..]
109                } else {
110                    element_name
111                };
112
113                return map_document_element_to_message_type(element_name);
114            }
115        }
116    }
117
118    Err(DataflowError::Validation(
119        "Could not extract message type from XML".to_string(),
120    ))
121}
122
123/// Extract MX content from a message field
124///
125/// This function handles various formats of MX message content:
126/// - Direct string values
127/// - Objects with mx_message field (from generate_mx output)
128/// - Payload field
129///
130/// # Arguments
131/// * `message_data` - The message data object
132/// * `field_name` - The field name to extract from
133/// * `message_payload` - The message payload (if field_name is "payload")
134///
135/// # Returns
136/// * Ok(content) - The extracted MX message content
137/// * Err(error) - If content cannot be extracted
138pub fn extract_mx_content(
139    message_data: &Value,
140    field_name: &str,
141    message_payload: &Value,
142) -> Result<String> {
143    if field_name == "payload" {
144        // Extract string value from the payload JSON
145        if let Some(s) = message_payload.as_str() {
146            Ok(s.to_string())
147        } else {
148            // If it's not a string directly, try to convert
149            Ok(message_payload.to_string().trim_matches('"').to_string())
150        }
151    } else {
152        // Check if the field contains an object with mx_message (from generate_mx output)
153        let field_value = message_data.get(field_name).ok_or_else(|| {
154            DataflowError::Validation(format!(
155                "MX message field '{}' not found in message data",
156                field_name
157            ))
158        })?;
159
160        // If it's an object with mx_message field, extract that
161        if let Some(mx_msg) = field_value.get("mx_message").and_then(Value::as_str) {
162            Ok(mx_msg.to_string())
163        } else if let Some(s) = field_value.as_str() {
164            // If it's a direct string, use it
165            Ok(s.to_string())
166        } else {
167            Err(DataflowError::Validation(format!(
168                "Field '{}' does not contain a valid MX message",
169                field_name
170            )))
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use serde_json::json;
179
180    #[test]
181    fn test_map_document_element_to_message_type() {
182        assert_eq!(
183            map_document_element_to_message_type("FIToFICstmrCdtTrf").unwrap(),
184            "pacs.008"
185        );
186        assert_eq!(
187            map_document_element_to_message_type("BkToCstmrStmt").unwrap(),
188            "camt.053"
189        );
190        assert_eq!(
191            map_document_element_to_message_type("CstmrCdtTrfInitn").unwrap(),
192            "pain.001"
193        );
194        assert!(map_document_element_to_message_type("UnknownElement").is_err());
195    }
196
197    #[test]
198    fn test_extract_message_type() {
199        // Test with message_type at root
200        let data = json!({"message_type": "pacs.008", "other": "data"});
201        assert_eq!(extract_message_type(&data).unwrap(), "pacs.008");
202
203        // Test with Document structure
204        let data = json!({
205            "Document": {
206                "FIToFICstmrCdtTrf": {
207                    "GrpHdr": {}
208                }
209            }
210        });
211        assert_eq!(extract_message_type(&data).unwrap(), "pacs.008");
212
213        // Test with no message type
214        let data = json!({"other": "data"});
215        assert!(extract_message_type(&data).is_err());
216    }
217
218    #[test]
219    fn test_extract_mx_content() {
220        let payload = json!("test content");
221        let data = json!({
222            "field1": "direct string",
223            "field2": {
224                "mx_message": "nested message"
225            }
226        });
227
228        // Test payload extraction
229        assert_eq!(
230            extract_mx_content(&data, "payload", &payload).unwrap(),
231            "test content"
232        );
233
234        // Test direct string field
235        assert_eq!(
236            extract_mx_content(&data, "field1", &payload).unwrap(),
237            "direct string"
238        );
239
240        // Test nested mx_message field
241        assert_eq!(
242            extract_mx_content(&data, "field2", &payload).unwrap(),
243            "nested message"
244        );
245
246        // Test missing field
247        assert!(extract_mx_content(&data, "missing", &payload).is_err());
248    }
249}