swift_mt_message/
parser.rs

1use std::collections::HashMap;
2
3use crate::errors::{ParseError, Result};
4use crate::headers::{ApplicationHeader, BasicHeader, Trailer, UserHeader};
5use crate::{RawBlocks, SwiftMessage, SwiftMessageBody};
6
7/// Type alias for the complex return type of field parsing
8type FieldParseResult = Result<(HashMap<String, Vec<String>>, Vec<String>)>;
9
10/// Main parser for SWIFT MT messages
11pub struct SwiftParser;
12
13impl SwiftParser {
14    /// Parse a raw SWIFT message string into a typed message
15    pub fn parse<T: SwiftMessageBody>(raw_message: &str) -> Result<SwiftMessage<T>> {
16        let blocks = Self::extract_blocks(raw_message)?;
17
18        // Parse headers
19        let basic_header = BasicHeader::parse(&blocks.block1.clone().unwrap_or_default())?;
20        let application_header =
21            ApplicationHeader::parse(&blocks.block2.clone().unwrap_or_default())?;
22        let user_header = blocks
23            .block3
24            .as_ref()
25            .map(|b| UserHeader::parse(b))
26            .transpose()?;
27        let trailer = blocks
28            .block5
29            .as_ref()
30            .map(|b| Trailer::parse(b))
31            .transpose()?;
32
33        // Extract message type from application header
34        let message_type = application_header.message_type.clone();
35
36        // Validate message type matches expected type
37        if message_type != T::message_type() {
38            return Err(ParseError::WrongMessageType {
39                expected: T::message_type().to_string(),
40                actual: message_type,
41            });
42        }
43
44        // Parse block 4 fields
45        let (field_map, field_order) = Self::parse_block4_fields(&blocks.block4)?;
46
47        // Parse message body using the field map
48        let fields = T::from_fields(field_map)?;
49
50        Ok(SwiftMessage {
51            basic_header,
52            application_header,
53            user_header,
54            trailer,
55            blocks,
56            message_type,
57            field_order,
58            fields,
59        })
60    }
61
62    /// Extract message blocks from raw SWIFT message
63    fn extract_blocks(raw_message: &str) -> Result<RawBlocks> {
64        let mut blocks = RawBlocks::default();
65
66        // Find block boundaries
67        let mut current_pos = 0;
68
69        // Block 1: Basic Header {1:...}
70        if let Some(start) = raw_message[current_pos..].find("{1:") {
71            let start = current_pos + start;
72            if let Some(end) = raw_message[start..].find('}') {
73                let end = start + end;
74                blocks.block1 = Some(raw_message[start + 3..end].to_string());
75                current_pos = end + 1;
76            }
77        }
78
79        // Block 2: Application Header {2:...}
80        if let Some(start) = raw_message[current_pos..].find("{2:") {
81            let start = current_pos + start;
82            if let Some(end) = raw_message[start..].find('}') {
83                let end = start + end;
84                blocks.block2 = Some(raw_message[start + 3..end].to_string());
85                current_pos = end + 1;
86            }
87        }
88
89        // Block 3: User Header {3:...} (optional)
90        if let Some(start) = raw_message[current_pos..].find("{3:") {
91            let start = current_pos + start;
92            // Find matching closing brace for block 3
93            if let Some(end) = Self::find_matching_brace(&raw_message[start..]) {
94                let end = start + end;
95                blocks.block3 = Some(raw_message[start + 3..end].to_string());
96                current_pos = end + 1;
97            }
98        }
99
100        // Block 4: Text Block {4:\n...-}
101        if let Some(start) = raw_message[current_pos..].find("{4:") {
102            let start = current_pos + start;
103            if let Some(end) = raw_message[start..].find("-}") {
104                let end = start + end;
105                blocks.block4 = raw_message[start + 3..end].to_string();
106                current_pos = end + 2;
107            }
108        }
109
110        // Block 5: Trailer {5:...} (optional)
111        if let Some(start) = raw_message[current_pos..].find("{5:") {
112            let start = current_pos + start;
113            if let Some(end) = raw_message[start..].find('}') {
114                let end = start + end;
115                blocks.block5 = Some(raw_message[start + 3..end].to_string());
116            }
117        }
118
119        if blocks.block1.is_none() || blocks.block2.is_none() || blocks.block4.is_empty() {
120            return Err(ParseError::InvalidBlockStructure {
121                message: "Missing required blocks (1, 2, or 4)".to_string(),
122            });
123        }
124
125        Ok(blocks)
126    }
127
128    /// Parse block 4 fields into a field map and preserve field order
129    fn parse_block4_fields(block4: &str) -> FieldParseResult {
130        let mut field_map: HashMap<String, Vec<String>> = HashMap::new();
131        let mut field_order = Vec::new();
132
133        // Remove leading/trailing whitespace and newlines
134        let content = block4.trim();
135
136        // Split by field markers (:XX:)
137        let mut current_pos = 0;
138
139        while current_pos < content.len() {
140            // Find next field marker
141            if let Some(field_start) = content[current_pos..].find(':') {
142                let field_start = current_pos + field_start;
143
144                // Extract field tag (characters after : until next :)
145                if let Some(tag_end) = content[field_start + 1..].find(':') {
146                    let tag_end = field_start + 1 + tag_end;
147                    let raw_field_tag = content[field_start + 1..tag_end].to_string();
148
149                    // Normalize field tag by removing option letters (A, F, K, etc.)
150                    let field_tag = Self::normalize_field_tag(&raw_field_tag);
151
152                    // Find the end of field value (next field marker or end of content)
153                    let value_start = tag_end + 1;
154                    let value_end = if let Some(next_field) = content[value_start..].find("\n:") {
155                        value_start + next_field
156                    } else {
157                        content.len()
158                    };
159
160                    let field_value = content[value_start..value_end].trim().to_string();
161
162                    // Store the complete field string including tag prefix for compatibility
163                    let complete_field_string = format!(":{}:{}", raw_field_tag, field_value);
164
165                    // Add to existing Vec or create new Vec for this field tag
166                    field_map
167                        .entry(field_tag.clone())
168                        .or_default()
169                        .push(complete_field_string);
170
171                    // Only add to field_order if this is the first occurrence of this field
172                    if !field_order.contains(&field_tag) {
173                        field_order.push(field_tag);
174                    }
175
176                    current_pos = value_end;
177                } else {
178                    // Last field or malformed
179                    break;
180                }
181            } else {
182                break;
183            }
184        }
185
186        Ok((field_map, field_order))
187    }
188
189    /// Normalize field tag by removing option letters (A, F, K, etc.)
190    /// Example: "50K" -> "50", "59A" -> "59", "20" -> "20"
191    /// But preserve option letters for fields that have multiple variants like 23B/23E, 71A/71F/71G
192    fn normalize_field_tag(raw_tag: &str) -> String {
193        // Extract the numeric part at the beginning
194        let mut numeric_part = String::new();
195        for ch in raw_tag.chars() {
196            if ch.is_ascii_digit() {
197                numeric_part.push(ch);
198            } else {
199                break;
200            }
201        }
202
203        // If we have letters after the number, check if it's a known option
204        if numeric_part.len() < raw_tag.len() {
205            let remaining = &raw_tag[numeric_part.len()..];
206
207            // For certain field numbers, preserve the option letter to avoid conflicts
208            match numeric_part.as_str() {
209                "13" | "23" | "26" | "32" | "33" | "52" | "53" | "54" | "55" | "56" | "57"
210                | "58" | "71" | "77" => {
211                    // Keep option letters for fields that have multiple variants or specific formats
212                    // 13C (Time Indication)
213                    // 23B (Bank Operation Code) vs 23E (Instruction Code)
214                    // 26T (Transaction Type Code)
215                    // 32A (Value Date/Currency/Amount)
216                    // 33B (Currency/Instructed Amount)
217                    // 52A (Ordering Institution)
218                    // 53A (Sender's Correspondent)
219                    // 54A (Receiver's Correspondent)
220                    // 55A (Third Reimbursement Institution)
221                    // 56A (Intermediary Institution)
222                    // 57A (Account With Institution)
223                    // 71A (Details of Charges) vs 71F (Sender's Charges) vs 71G (Receiver's Charges)
224                    // 77B (Regulatory Reporting)
225                    return raw_tag.to_string();
226                }
227                _ => {
228                    // For other fields, remove option letters as before
229                    if remaining
230                        .chars()
231                        .all(|c| c.is_ascii_alphabetic() && c.is_ascii_uppercase())
232                    {
233                        // It's an option letter, return just the numeric part
234                        return numeric_part;
235                    }
236                }
237            }
238        }
239
240        // If no option letter found, return the original tag
241        raw_tag.to_string()
242    }
243
244    /// Find the matching closing brace for a block that starts with an opening brace
245    /// Handles nested braces correctly
246    fn find_matching_brace(text: &str) -> Option<usize> {
247        let mut chars = text.char_indices();
248
249        // Skip the first character (should be '{')
250        let mut brace_count = if let Some((_, '{')) = chars.next() {
251            1
252        } else {
253            return None;
254        };
255
256        for (i, ch) in chars {
257            match ch {
258                '{' => brace_count += 1,
259                '}' => {
260                    brace_count -= 1;
261                    if brace_count == 0 {
262                        return Some(i);
263                    }
264                }
265                _ => {}
266            }
267        }
268
269        None
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn test_extract_blocks() {
279        let raw_message = "{1:F01BANKDEFFAXXX0123456789}{2:I103BANKDEFFAXXXU3003}{4:\n:20:FT21234567890\n:23B:CRED\n-}";
280        let blocks = SwiftParser::extract_blocks(raw_message).unwrap();
281
282        assert!(blocks.block1.is_some());
283        assert!(blocks.block2.is_some());
284        assert!(!blocks.block4.is_empty());
285        assert_eq!(blocks.block1.as_ref().unwrap(), "F01BANKDEFFAXXX0123456789");
286        assert_eq!(blocks.block2.as_ref().unwrap(), "I103BANKDEFFAXXXU3003");
287    }
288
289    #[test]
290    fn test_parse_block4_fields() {
291        let block4 = "\n:20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n";
292        let (field_map, field_order) = SwiftParser::parse_block4_fields(block4).unwrap();
293
294        assert_eq!(
295            field_map.get("20"),
296            Some(&vec![":20:FT21234567890".to_string()])
297        );
298        assert_eq!(field_map.get("23B"), Some(&vec![":23B:CRED".to_string()]));
299        assert_eq!(
300            field_map.get("32A"),
301            Some(&vec![":32A:210315EUR1234567,89".to_string()])
302        );
303
304        assert_eq!(field_order, vec!["20", "23B", "32A"]);
305    }
306
307    #[test]
308    fn test_debug_mt103_fields() {
309        let block4 = r#"
310:20:FT21001234567890
311:23B:CRED
312:32A:240101USD1000,00
313:50K:/1234567890
314ACME CORPORATION
315123 MAIN STREET
316NEW YORK NY 10001
317:52A:BNPAFRPPXXX
318:57A:DEUTDEFFXXX
319:59:/DE89370400440532013000
320MUELLER GMBH
321HAUPTSTRASSE 1
32210115 BERLIN
323:70:PAYMENT FOR INVOICE 12345
324:71A:OUR
325"#;
326        let (field_map, field_order) = SwiftParser::parse_block4_fields(block4).unwrap();
327
328        println!("Extracted fields:");
329        for (tag, values) in &field_map {
330            println!("  {}: {:?}", tag, values);
331        }
332        println!("Field order: {:?}", field_order);
333
334        // Check specific fields
335        assert!(field_map.contains_key("20"));
336        assert!(field_map.contains_key("23B"));
337        assert!(field_map.contains_key("32A"));
338        assert!(field_map.contains_key("50"));
339        assert!(field_map.contains_key("52A"));
340        assert!(field_map.contains_key("57A"));
341        assert!(field_map.contains_key("59"));
342        assert!(field_map.contains_key("70"));
343        assert!(field_map.contains_key("71A"));
344    }
345
346    #[test]
347    fn test_block3_parsing_with_nested_tags() {
348        let raw_message = r#"{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}{4:
349:20:FT21001234567890
350:23B:CRED
351-}"#;
352
353        let blocks = SwiftParser::extract_blocks(raw_message).unwrap();
354
355        assert!(blocks.block3.is_some());
356        let block3_content = blocks.block3.unwrap();
357        println!("Block 3 content: '{}'", block3_content);
358
359        // Should contain both tags
360        assert_eq!(
361            block3_content,
362            "{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}"
363        );
364        assert!(block3_content.contains("103:EBA"));
365        assert!(block3_content.contains("121:180f1e65-90e0-44d5-a49a-92b55eb3025f"));
366    }
367
368    #[test]
369    fn test_find_matching_brace() {
370        // Simple case: "{simple}" -> closing } at position 7
371        assert_eq!(SwiftParser::find_matching_brace("{simple}"), Some(7));
372
373        // Nested braces: "{outer{inner}outer}" -> closing } at position 18
374        assert_eq!(
375            SwiftParser::find_matching_brace("{outer{inner}outer}"),
376            Some(18)
377        );
378
379        // SWIFT block 3 case: "{{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}"
380        let test_str = "{{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}}";
381        let expected_pos = test_str.len() - 1; // Last character position
382        assert_eq!(
383            SwiftParser::find_matching_brace(test_str),
384            Some(expected_pos)
385        );
386
387        // Simple nested: "{103:EBA}" -> closing } at position 8
388        assert_eq!(SwiftParser::find_matching_brace("{103:EBA}"), Some(8));
389
390        // No closing brace
391        assert_eq!(SwiftParser::find_matching_brace("{no_close"), None);
392
393        // Not starting with brace
394        assert_eq!(SwiftParser::find_matching_brace("no_brace"), None);
395    }
396
397    #[test]
398    fn debug_find_matching_brace() {
399        let test_str = "{103:EBA}{121:180f1e65-90e0-44d5-a49a-92b55eb3025f}";
400        println!("Test string: '{}'", test_str);
401        println!("Length: {}", test_str.len());
402
403        let result = SwiftParser::find_matching_brace(test_str);
404        println!("Result: {:?}", result);
405
406        // Let's manually check what character is at different positions
407        for (i, ch) in test_str.char_indices() {
408            println!("Position {}: '{}'", i, ch);
409        }
410    }
411}