swift_mt_message/
parser.rs

1use std::collections::HashMap;
2
3use crate::errors::{ParseError, Result};
4use crate::headers::{ApplicationHeader, BasicHeader, Trailer, UserHeader};
5use crate::messages::{MT103, MT202, MT205, MT900};
6use crate::{ParsedSwiftMessage, RawBlocks, SwiftMessage, SwiftMessageBody};
7
8/// Type alias for the complex return type of field parsing
9type FieldParseResult = Result<(HashMap<String, Vec<String>>, Vec<String>)>;
10
11/// Main parser for SWIFT MT messages
12pub struct SwiftParser;
13
14impl SwiftParser {
15    /// Parse a raw SWIFT message string into a typed message
16    pub fn parse<T: SwiftMessageBody>(raw_message: &str) -> Result<SwiftMessage<T>> {
17        let blocks = Self::extract_blocks(raw_message)?;
18
19        // Parse headers
20        let basic_header = BasicHeader::parse(&blocks.block1.clone().unwrap_or_default())?;
21        let application_header =
22            ApplicationHeader::parse(&blocks.block2.clone().unwrap_or_default())?;
23        let user_header = blocks
24            .block3
25            .as_ref()
26            .map(|b| UserHeader::parse(b))
27            .transpose()?;
28        let trailer = blocks
29            .block5
30            .as_ref()
31            .map(|b| Trailer::parse(b))
32            .transpose()?;
33
34        // Extract message type from application header
35        let message_type = application_header.message_type.clone();
36
37        // Validate message type matches expected type
38        if message_type != T::message_type() {
39            return Err(ParseError::WrongMessageType {
40                expected: T::message_type().to_string(),
41                actual: message_type,
42            });
43        }
44
45        // Parse block 4 fields
46        let (field_map, field_order) = Self::parse_block4_fields(&blocks.block4)?;
47
48        // Parse message body using the field map
49        let fields = T::from_fields(field_map)?;
50
51        Ok(SwiftMessage {
52            basic_header,
53            application_header,
54            user_header,
55            trailer,
56            blocks,
57            message_type,
58            field_order,
59            fields,
60        })
61    }
62
63    /// Parse a raw SWIFT message string with automatic message type detection
64    pub fn parse_auto(raw_message: &str) -> Result<ParsedSwiftMessage> {
65        // First, extract blocks to get the message type
66        let blocks = Self::extract_blocks(raw_message)?;
67
68        // Parse application header to get message type
69        let application_header =
70            ApplicationHeader::parse(&blocks.block2.clone().unwrap_or_default())?;
71        let message_type = &application_header.message_type;
72
73        // Route to appropriate parser based on message type
74        match message_type.as_str() {
75            "103" => {
76                let parsed = Self::parse::<MT103>(raw_message)?;
77                Ok(ParsedSwiftMessage::MT103(Box::new(parsed)))
78            }
79            "202" => {
80                let parsed = Self::parse::<MT202>(raw_message)?;
81                Ok(ParsedSwiftMessage::MT202(Box::new(parsed)))
82            }
83            "205" => {
84                let parsed = Self::parse::<MT205>(raw_message)?;
85                Ok(ParsedSwiftMessage::MT205(Box::new(parsed)))
86            }
87            "900" => {
88                let parsed = Self::parse::<MT900>(raw_message)?;
89                Ok(ParsedSwiftMessage::MT900(Box::new(parsed)))
90            }
91            _ => Err(ParseError::UnsupportedMessageType {
92                message_type: message_type.clone(),
93            }),
94        }
95    }
96
97    /// Extract message blocks from raw SWIFT message
98    pub fn extract_blocks(raw_message: &str) -> Result<RawBlocks> {
99        let mut blocks = RawBlocks::default();
100
101        // Find block boundaries
102        let mut current_pos = 0;
103
104        // Block 1: Basic Header {1:...}
105        if let Some(start) = raw_message[current_pos..].find("{1:") {
106            let start = current_pos + start;
107            if let Some(end) = raw_message[start..].find('}') {
108                let end = start + end;
109                blocks.block1 = Some(raw_message[start + 3..end].to_string());
110                current_pos = end + 1;
111            }
112        }
113
114        // Block 2: Application Header {2:...}
115        if let Some(start) = raw_message[current_pos..].find("{2:") {
116            let start = current_pos + start;
117            if let Some(end) = raw_message[start..].find('}') {
118                let end = start + end;
119                blocks.block2 = Some(raw_message[start + 3..end].to_string());
120                current_pos = end + 1;
121            }
122        }
123
124        // Block 3: User Header {3:...} (optional)
125        if let Some(start) = raw_message[current_pos..].find("{3:") {
126            let start = current_pos + start;
127            // Find matching closing brace for block 3
128            if let Some(end) = Self::find_matching_brace(&raw_message[start..]) {
129                let end = start + end;
130                blocks.block3 = Some(raw_message[start + 3..end].to_string());
131                current_pos = end + 1;
132            }
133        }
134
135        // Block 4: Text Block {4:\n...-}
136        if let Some(start) = raw_message[current_pos..].find("{4:") {
137            let start = current_pos + start;
138            if let Some(end) = raw_message[start..].find("-}") {
139                let end = start + end;
140                blocks.block4 = raw_message[start + 3..end].to_string();
141                current_pos = end + 2;
142            }
143        }
144
145        // Block 5: Trailer {5:...} (optional)
146        if let Some(start) = raw_message[current_pos..].find("{5:") {
147            let start = current_pos + start;
148            if let Some(end) = raw_message[start..].find('}') {
149                let end = start + end;
150                blocks.block5 = Some(raw_message[start + 3..end].to_string());
151            }
152        }
153
154        if blocks.block1.is_none() || blocks.block2.is_none() || blocks.block4.is_empty() {
155            return Err(ParseError::InvalidBlockStructure {
156                message: "Missing required blocks (1, 2, or 4)".to_string(),
157            });
158        }
159
160        Ok(blocks)
161    }
162
163    /// Parse block 4 fields into a field map and preserve field order
164    fn parse_block4_fields(block4: &str) -> FieldParseResult {
165        let mut field_map: HashMap<String, Vec<String>> = HashMap::new();
166        let mut field_order = Vec::new();
167
168        // Remove leading/trailing whitespace and newlines
169        let content = block4.trim();
170
171        // Split by field markers (:XX:)
172        let mut current_pos = 0;
173
174        while current_pos < content.len() {
175            // Find next field marker
176            if let Some(field_start) = content[current_pos..].find(':') {
177                let field_start = current_pos + field_start;
178
179                // Extract field tag (characters after : until next :)
180                if let Some(tag_end) = content[field_start + 1..].find(':') {
181                    let tag_end = field_start + 1 + tag_end;
182                    let raw_field_tag = content[field_start + 1..tag_end].to_string();
183
184                    // Normalize field tag by removing option letters (A, F, K, etc.)
185                    let field_tag = Self::normalize_field_tag(&raw_field_tag);
186
187                    // Find the end of field value (next field marker or end of content)
188                    let value_start = tag_end + 1;
189                    let value_end = if let Some(next_field) = content[value_start..].find("\n:") {
190                        value_start + next_field
191                    } else {
192                        content.len()
193                    };
194
195                    let field_value = content[value_start..value_end].trim().to_string();
196
197                    // Store the complete field string including tag prefix for compatibility
198                    let complete_field_string = format!(":{}:{}", raw_field_tag, field_value);
199
200                    // Add to existing Vec or create new Vec for this field tag
201                    field_map
202                        .entry(field_tag.clone())
203                        .or_default()
204                        .push(complete_field_string);
205
206                    // Only add to field_order if this is the first occurrence of this field
207                    if !field_order.contains(&field_tag) {
208                        field_order.push(field_tag);
209                    }
210
211                    current_pos = value_end;
212                } else {
213                    // Last field or malformed
214                    break;
215                }
216            } else {
217                break;
218            }
219        }
220
221        Ok((field_map, field_order))
222    }
223
224    /// Normalize field tag by removing option letters (A, F, K, etc.)
225    /// Example: "50K" -> "50", "59A" -> "59", "20" -> "20"
226    /// But preserve option letters for fields that have multiple variants like 23B/23E, 71A/71F/71G
227    fn normalize_field_tag(raw_tag: &str) -> String {
228        // Extract the numeric part at the beginning
229        let mut numeric_part = String::new();
230        for ch in raw_tag.chars() {
231            if ch.is_ascii_digit() {
232                numeric_part.push(ch);
233            } else {
234                break;
235            }
236        }
237
238        // If we have letters after the number, check if it's a known option
239        if numeric_part.len() < raw_tag.len() {
240            let remaining = &raw_tag[numeric_part.len()..];
241
242            // For certain field numbers, preserve the option letter to avoid conflicts
243            match numeric_part.as_str() {
244                "13" | "23" | "26" | "32" | "33" | "52" | "53" | "54" | "55" | "56" | "57"
245                | "58" | "71" | "77" => {
246                    // Keep option letters for fields that have multiple variants or specific formats
247                    // 13C (Time Indication)
248                    // 23B (Bank Operation Code) vs 23E (Instruction Code)
249                    // 26T (Transaction Type Code)
250                    // 32A (Value Date/Currency/Amount)
251                    // 33B (Currency/Instructed Amount)
252                    // 52A (Ordering Institution)
253                    // 53A (Sender's Correspondent)
254                    // 54A (Receiver's Correspondent)
255                    // 55A (Third Reimbursement Institution)
256                    // 56A (Intermediary Institution)
257                    // 57A (Account With Institution)
258                    // 71A (Details of Charges) vs 71F (Sender's Charges) vs 71G (Receiver's Charges)
259                    // 77B (Regulatory Reporting)
260                    return raw_tag.to_string();
261                }
262                _ => {
263                    // For other fields, remove option letters as before
264                    if remaining
265                        .chars()
266                        .all(|c| c.is_ascii_alphabetic() && c.is_ascii_uppercase())
267                    {
268                        // It's an option letter, return just the numeric part
269                        return numeric_part;
270                    }
271                }
272            }
273        }
274
275        // If no option letter found, return the original tag
276        raw_tag.to_string()
277    }
278
279    /// Find the matching closing brace for a block that starts with an opening brace
280    /// Handles nested braces correctly
281    fn find_matching_brace(text: &str) -> Option<usize> {
282        let mut chars = text.char_indices();
283
284        // Skip the first character (should be '{')
285        let mut brace_count = if let Some((_, '{')) = chars.next() {
286            1
287        } else {
288            return None;
289        };
290
291        for (i, ch) in chars {
292            match ch {
293                '{' => brace_count += 1,
294                '}' => {
295                    brace_count -= 1;
296                    if brace_count == 0 {
297                        return Some(i);
298                    }
299                }
300                _ => {}
301            }
302        }
303
304        None
305    }
306}
307
308/// Parse a SwiftMessage from a string representation
309/// This is a placeholder implementation for the macro system
310pub fn parse_swift_message_from_string(value: &str) -> Result<HashMap<String, Vec<String>>> {
311    // For now, this is a stub implementation
312    // In a real implementation, this would parse the string representation
313    // of a SwiftMessage back into a field map
314
315    // As a temporary solution, we'll assume the value is a simple field representation
316    // and try to parse it as a mini SWIFT block
317    let mut field_map = HashMap::new();
318
319    // Split by lines and parse each field
320    for line in value.lines() {
321        if line.trim().is_empty() {
322            continue;
323        }
324
325        // Look for field pattern :XX:value
326        if let Some(colon_pos) = line.find(':') {
327            if let Some(second_colon) = line[colon_pos + 1..].find(':') {
328                let second_colon_pos = colon_pos + 1 + second_colon;
329                let field_tag = line[colon_pos + 1..second_colon_pos].to_string();
330                let _field_value = line[second_colon_pos + 1..].to_string();
331
332                field_map
333                    .entry(field_tag)
334                    .or_insert_with(Vec::new)
335                    .push(format!(":{}", &line[colon_pos + 1..]));
336            }
337        }
338    }
339
340    Ok(field_map)
341}
342
343/// Serialize a SwiftMessage field map to a string representation
344/// This is a placeholder implementation for the macro system
345pub fn serialize_swift_message_to_string(fields: &HashMap<String, Vec<String>>) -> String {
346    // For now, this is a stub implementation
347    // In a real implementation, this would serialize the field map
348    // into a string representation of a SwiftMessage
349
350    let mut result = String::new();
351
352    // Simple serialization: just join all field values with newlines
353    for field_values in fields.values() {
354        for field_value in field_values {
355            // field_value should already be in the format ":XX:value"
356            result.push_str(field_value);
357            result.push('\n');
358        }
359    }
360
361    // Remove trailing newline
362    if result.ends_with('\n') {
363        result.pop();
364    }
365
366    result
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_extract_blocks() {
375        let raw_message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:\n:20:FT21234567890\n:23B:CRED\n-}";
376        let blocks = SwiftParser::extract_blocks(raw_message).unwrap();
377
378        assert!(blocks.block1.is_some());
379        assert!(blocks.block2.is_some());
380        assert!(!blocks.block4.is_empty());
381        assert_eq!(blocks.block1.as_ref().unwrap(), "F01BANKDEFFAXXX0123456789");
382        assert_eq!(blocks.block2.as_ref().unwrap(), "I103BANKDEFFAXXXU3003");
383    }
384
385    #[test]
386    fn test_parse_block4_fields() {
387        let block4 = "\n:20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n";
388        let (field_map, field_order) = SwiftParser::parse_block4_fields(block4).unwrap();
389
390        assert_eq!(
391            field_map.get("20"),
392            Some(&vec![":20:FT21234567890".to_string()])
393        );
394        assert_eq!(field_map.get("23B"), Some(&vec![":23B:CRED".to_string()]));
395        assert_eq!(
396            field_map.get("32A"),
397            Some(&vec![":32A:210315EUR1234567,89".to_string()])
398        );
399
400        assert_eq!(field_order, vec!["20", "23B", "32A"]);
401    }
402
403    #[test]
404    fn test_debug_mt103_fields() {
405        let block4 = r#"
406:20:FT21001234567890
407:23B:CRED
408:32A:240101USD1000,00
409:50K:/1234567890
410ACME CORPORATION
411123 MAIN STREET
412NEW YORK NY 10001
413:52A:BNPAFRPPXXX
414:57A:DEUTDEFFXXX
415:59:/DE89370400440532013000
416MUELLER GMBH
417HAUPTSTRASSE 1
41810115 BERLIN
419:70:PAYMENT FOR INVOICE 12345
420:71A:OUR
421"#;
422        let (field_map, field_order) = SwiftParser::parse_block4_fields(block4).unwrap();
423
424        println!("Extracted fields:");
425        for (tag, values) in &field_map {
426            println!("  {}: {:?}", tag, values);
427        }
428        println!("Field order: {:?}", field_order);
429
430        // Check specific fields
431        assert!(field_map.contains_key("20"));
432        assert!(field_map.contains_key("23B"));
433        assert!(field_map.contains_key("32A"));
434        assert!(field_map.contains_key("50"));
435        assert!(field_map.contains_key("52A"));
436        assert!(field_map.contains_key("57A"));
437        assert!(field_map.contains_key("59"));
438        assert!(field_map.contains_key("70"));
439        assert!(field_map.contains_key("71A"));
440    }
441
442    #[test]
443    fn test_block3_parsing_with_nested_tags() {
444        let raw_message = r#"{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}{4:
445:20:FT21001234567890
446:23B:CRED
447-}"#;
448
449        let blocks = SwiftParser::extract_blocks(raw_message).unwrap();
450
451        assert!(blocks.block3.is_some());
452        let block3_content = blocks.block3.unwrap();
453        println!("Block 3 content: '{}'", block3_content);
454
455        // Should contain both tags
456        assert_eq!(
457            block3_content,
458            "{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}"
459        );
460        assert!(block3_content.contains("103:EBA"));
461        assert!(block3_content.contains("121:180f1e65-90e0-44d5-a49a-92b55eb3025f"));
462    }
463
464    #[test]
465    fn test_find_matching_brace() {
466        // Simple case: "{simple}" -> closing } at position 7
467        assert_eq!(SwiftParser::find_matching_brace("{simple}"), Some(7));
468
469        // Nested braces: "{outer{inner}outer}" -> closing } at position 18
470        assert_eq!(
471            SwiftParser::find_matching_brace("{outer{inner}outer}"),
472            Some(18)
473        );
474
475        // SWIFT block 3 case: "{{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}"
476        let test_str = "{{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}";
477        let expected_pos = test_str.len() - 1; // Last character position
478        assert_eq!(
479            SwiftParser::find_matching_brace(test_str),
480            Some(expected_pos)
481        );
482
483        // Simple nested: "{103:EBA}" -> closing } at position 8
484        assert_eq!(SwiftParser::find_matching_brace("{103:EBA}"), Some(8));
485
486        // No closing brace
487        assert_eq!(SwiftParser::find_matching_brace("{no_close"), None);
488
489        // Not starting with brace
490        assert_eq!(SwiftParser::find_matching_brace("no_brace"), None);
491    }
492
493    #[test]
494    fn debug_find_matching_brace() {
495        let test_str = "{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}";
496        println!("Test string: '{}'", test_str);
497        println!("Length: {}", test_str.len());
498
499        let result = SwiftParser::find_matching_brace(test_str);
500        println!("Result: {:?}", result);
501
502        // Let's manually check what character is at different positions
503        for (i, ch) in test_str.char_indices() {
504            println!("Position {}: '{}'", i, ch);
505        }
506    }
507
508    #[test]
509    fn test_parse_auto_mt103() {
510        let raw_mt103 = r#"{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:
511:20:FT21234567890
512:23B:CRED
513:32A:210315EUR1234567,89
514:50K:ACME CORPORATION
515123 BUSINESS AVENUE
516NEW YORK NY 10001
517:52A:BANKDEFF
518:57A:DEUTDEFF
519:59A:/DE89370400440532013000
520DEUTDEFF
521:70:PAYMENT FOR SERVICES
522:71A:OUR
523-}"#;
524
525        let parsed = SwiftParser::parse_auto(raw_mt103).unwrap();
526
527        // Check that it detected the correct message type
528        assert_eq!(parsed.message_type(), "103");
529
530        // Check that we can extract the MT103 message
531        let mt103_msg = parsed.as_mt103().unwrap();
532        assert_eq!(mt103_msg.message_type, "103");
533
534        println!("Successfully parsed MT103 message with auto-detection");
535    }
536
537    #[test]
538    fn test_parse_auto_unsupported_type() {
539        let raw_message = r#"{1:F01BANKDEFFAXXX0123456789}{2:I999BANKDEFFAXXXU3003}{4:
540:20:FT21234567890
541-}"#;
542
543        let result = SwiftParser::parse_auto(raw_message);
544        assert!(result.is_err());
545
546        if let Err(ParseError::UnsupportedMessageType { message_type }) = result {
547            assert_eq!(message_type, "999");
548        } else {
549            panic!("Expected UnsupportedMessageType error");
550        }
551    }
552}