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