swift_mt_message/
parser.rs

1//! Core parsing logic for SWIFT MT messages
2
3use regex::Regex;
4use std::collections::HashMap;
5
6use crate::common::{Field, MessageBlock};
7use crate::error::{MTError, Result};
8use crate::messages::{MTMessage, MTMessageType};
9
10/// Main parser for SWIFT MT messages
11pub struct MTParser {
12    block_regex: Regex,
13}
14
15impl MTParser {
16    pub fn new() -> Result<Self> {
17        let block_regex = Regex::new(r"\{(\d):([^}]*)\}")?;
18
19        Ok(Self { block_regex })
20    }
21
22    /// Parse a complete SWIFT MT message
23    pub fn parse(&self, input: &str) -> Result<MTMessage> {
24        let blocks = self.parse_blocks(input)?;
25        let message_type = self.extract_message_type(&blocks)?;
26
27        match message_type.as_str() {
28            "103" => {
29                let mt103 = crate::messages::mt103::MT103::from_blocks(blocks)?;
30                Ok(MTMessage::MT103(mt103))
31            }
32            "102" => {
33                let mt102 = crate::messages::mt102::MT102::from_blocks(blocks)?;
34                Ok(MTMessage::MT102(mt102))
35            }
36            "202" => {
37                // Check if this is a COV message by looking for customer fields
38                let text_block = self.extract_text_block(&blocks)?;
39                let has_ordering_customer = text_block.iter().any(|f| {
40                    f.tag.as_str() == "50K" || f.tag.as_str() == "50A" || f.tag.as_str() == "50F"
41                });
42                let has_beneficiary_customer = text_block.iter().any(|f| {
43                    f.tag.as_str() == "59" || f.tag.as_str() == "59A" || f.tag.as_str() == "59F"
44                });
45
46                if has_ordering_customer && has_beneficiary_customer {
47                    // This is MT202COV
48                    let mt202cov = crate::messages::mt202cov::MT202COV::from_blocks(blocks)?;
49                    Ok(MTMessage::MT202COV(mt202cov))
50                } else {
51                    // This is regular MT202
52                    let mt202 = crate::messages::mt202::MT202::from_blocks(blocks)?;
53                    Ok(MTMessage::MT202(mt202))
54                }
55            }
56            "210" => {
57                let mt210 = crate::messages::mt210::MT210::from_blocks(blocks)?;
58                Ok(MTMessage::MT210(mt210))
59            }
60            "940" => {
61                let mt940 = crate::messages::mt940::MT940::from_blocks(blocks)?;
62                Ok(MTMessage::MT940(mt940))
63            }
64            "941" => {
65                let mt941 = crate::messages::mt941::MT941::from_blocks(blocks)?;
66                Ok(MTMessage::MT941(mt941))
67            }
68            "942" => {
69                let mt942 = crate::messages::mt942::MT942::from_blocks(blocks)?;
70                Ok(MTMessage::MT942(mt942))
71            }
72            "192" => {
73                let mt192 = crate::messages::mt192::MT192::from_blocks(blocks)?;
74                Ok(MTMessage::MT192(mt192))
75            }
76            "195" => {
77                let mt195 = crate::messages::mt195::MT195::from_blocks(blocks)?;
78                Ok(MTMessage::MT195(mt195))
79            }
80            "196" => {
81                let mt196 = crate::messages::mt196::MT196::from_blocks(blocks)?;
82                Ok(MTMessage::MT196(mt196))
83            }
84            "197" => {
85                let mt197 = crate::messages::mt197::MT197::from_blocks(blocks)?;
86                Ok(MTMessage::MT197(mt197))
87            }
88            "199" => {
89                let mt199 = crate::messages::mt199::MT199::from_blocks(blocks)?;
90                Ok(MTMessage::MT199(mt199))
91            }
92            _ => Err(MTError::UnsupportedMessageType {
93                message_type: message_type.clone(),
94            }),
95        }
96    }
97
98    /// Parse message blocks from input
99    pub fn parse_blocks(&self, input: &str) -> Result<Vec<MessageBlock>> {
100        let mut blocks = Vec::new();
101
102        for cap in self.block_regex.captures_iter(input) {
103            let block_number = cap
104                .get(1)
105                .ok_or_else(|| MTError::ParseError {
106                    line: 1,
107                    column: 1,
108                    message: "Invalid block format".to_string(),
109                })?
110                .as_str();
111
112            let block_content = cap
113                .get(2)
114                .ok_or_else(|| MTError::ParseError {
115                    line: 1,
116                    column: 1,
117                    message: "Invalid block content".to_string(),
118                })?
119                .as_str();
120
121            match block_number {
122                "1" => blocks.push(self.parse_basic_header(block_content)?),
123                "2" => blocks.push(self.parse_application_header(block_content)?),
124                "3" => blocks.push(self.parse_user_header(block_content)?),
125                "4" => blocks.push(self.parse_text_block(block_content)?),
126                "5" => blocks.push(self.parse_trailer_block(block_content)?),
127                _ => {
128                    return Err(MTError::ParseError {
129                        line: 1,
130                        column: 1,
131                        message: format!("Unknown block number: {}", block_number),
132                    });
133                }
134            }
135        }
136
137        if blocks.is_empty() {
138            return Err(MTError::ParseError {
139                line: 1,
140                column: 1,
141                message: "No blocks found in message".to_string(),
142            });
143        }
144
145        Ok(blocks)
146    }
147
148    /// Parse basic header block (Block 1)
149    fn parse_basic_header(&self, content: &str) -> Result<MessageBlock> {
150        // Format: F01BANKDEFFAXXX0123456789
151        if content.len() < 21 {
152            return Err(MTError::InvalidMessageStructure {
153                message: "Basic header block too short".to_string(),
154            });
155        }
156
157        let application_id = content[0..1].to_string();
158        let service_id = content[1..3].to_string();
159        let logical_terminal = content[3..15].to_string();
160        let session_number = content[15..19].to_string();
161        let sequence_number = content[19..].to_string();
162
163        Ok(MessageBlock::BasicHeader {
164            application_id,
165            service_id,
166            logical_terminal,
167            session_number,
168            sequence_number,
169        })
170    }
171
172    /// Parse application header block (Block 2)
173    fn parse_application_header(&self, content: &str) -> Result<MessageBlock> {
174        // Format: I103BANKDEFFAXXXU3003 or O1031535010605BANKDEFFAXXXU3003
175        if content.is_empty() {
176            return Err(MTError::InvalidMessageStructure {
177                message: "Application header block is empty".to_string(),
178            });
179        }
180
181        let input_output_identifier = content[0..1].to_string();
182
183        if content.len() < 4 {
184            return Err(MTError::InvalidMessageStructure {
185                message: "Application header block too short".to_string(),
186            });
187        }
188
189        let message_type = content[1..4].to_string();
190        let remaining = &content[4..];
191
192        // Parse the rest based on input/output identifier
193        let (destination_address, priority, delivery_monitoring, obsolescence_period) =
194            if input_output_identifier == "I" {
195                // Input message format
196                if remaining.len() >= 12 {
197                    let dest = remaining[0..12].to_string();
198                    let prio = remaining.get(12..13).unwrap_or("").to_string();
199                    let del_mon = remaining.get(13..14).map(|s| s.to_string());
200                    let obs_per = remaining.get(14..17).map(|s| s.to_string());
201                    (dest, prio, del_mon, obs_per)
202                } else {
203                    (remaining.to_string(), String::new(), None, None)
204                }
205            } else {
206                // Output message format
207                (remaining.to_string(), String::new(), None, None)
208            };
209
210        Ok(MessageBlock::ApplicationHeader {
211            input_output_identifier,
212            message_type,
213            destination_address,
214            priority,
215            delivery_monitoring,
216            obsolescence_period,
217        })
218    }
219
220    /// Parse user header block (Block 3)
221    fn parse_user_header(&self, content: &str) -> Result<MessageBlock> {
222        let mut fields = HashMap::new();
223
224        // Parse user header fields (format: {tag:value})
225        let user_field_regex = Regex::new(r"\{(\w+):([^}]*)\}")?;
226        for captures in user_field_regex.captures_iter(content) {
227            let tag = captures[1].to_string();
228            let value = captures[2].to_string();
229            fields.insert(tag, value);
230        }
231
232        Ok(MessageBlock::UserHeader { fields })
233    }
234
235    /// Parse text block (Block 4)
236    fn parse_text_block(&self, content: &str) -> Result<MessageBlock> {
237        let mut fields = Vec::new();
238        let lines: Vec<&str> = content.lines().collect();
239
240        let mut current_tag = String::new();
241        let mut current_value = String::new();
242
243        for line in lines {
244            let line = line.trim();
245            if line.is_empty() || line == "-" {
246                continue;
247            }
248
249            if line.starts_with(':') && line.contains(':') {
250                // Save previous field if exists
251                if !current_tag.is_empty() {
252                    fields.push(Field::new(
253                        current_tag.clone(),
254                        current_value.trim().to_string(),
255                    ));
256                }
257
258                // Parse new field
259                if let Some(colon_pos) = line[1..].find(':') {
260                    current_tag = line[1..colon_pos + 1].to_string();
261                    current_value = line[colon_pos + 2..].to_string();
262                } else {
263                    return Err(MTError::ParseError {
264                        line: 0,
265                        column: 0,
266                        message: format!("Invalid field format: {}", line),
267                    });
268                }
269            } else {
270                // Continuation of previous field
271                if !current_value.is_empty() {
272                    current_value.push('\n');
273                }
274                current_value.push_str(line);
275            }
276        }
277
278        // Save last field
279        if !current_tag.is_empty() {
280            fields.push(Field::new(current_tag, current_value.trim().to_string()));
281        }
282
283        Ok(MessageBlock::TextBlock { fields })
284    }
285
286    /// Parse trailer block (Block 5)
287    fn parse_trailer_block(&self, content: &str) -> Result<MessageBlock> {
288        let mut fields = HashMap::new();
289
290        // Parse trailer fields (format: {tag:value})
291        let trailer_field_regex = Regex::new(r"\{(\w+):([^}]*)\}")?;
292        for captures in trailer_field_regex.captures_iter(content) {
293            let tag = captures[1].to_string();
294            let value = captures[2].to_string();
295            fields.insert(tag, value);
296        }
297
298        Ok(MessageBlock::TrailerBlock { fields })
299    }
300
301    /// Extract message type from blocks
302    pub fn extract_message_type(&self, blocks: &[MessageBlock]) -> Result<String> {
303        for block in blocks {
304            if let MessageBlock::ApplicationHeader { message_type, .. } = block {
305                return Ok(message_type.clone());
306            }
307        }
308
309        Err(MTError::ParseError {
310            line: 1,
311            column: 1,
312            message: "No application header block found".to_string(),
313        })
314    }
315
316    /// Extract text block fields for message type detection
317    fn extract_text_block(&self, blocks: &[MessageBlock]) -> Result<Vec<Field>> {
318        for block in blocks {
319            if let MessageBlock::TextBlock { fields } = block {
320                return Ok(fields.clone());
321            }
322        }
323
324        Err(MTError::ParseError {
325            line: 1,
326            column: 1,
327            message: "No text block found".to_string(),
328        })
329    }
330}
331
332impl Default for MTParser {
333    fn default() -> Self {
334        Self::new().expect("Failed to create default parser")
335    }
336}
337
338/// Parse a SWIFT MT message from text
339pub fn parse_message(input: &str) -> Result<MTMessage> {
340    let parser = MTParser::new()?;
341    parser.parse(input)
342}
343
344/// Extract fields from a text block
345pub fn extract_fields(text_block: &str) -> Result<Vec<Field>> {
346    let parser = MTParser::new()?;
347    if let MessageBlock::TextBlock { fields } = parser.parse_text_block(text_block)? {
348        Ok(fields)
349    } else {
350        Err(MTError::InvalidMessageStructure {
351            message: "Failed to parse text block".to_string(),
352        })
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_basic_header_parsing() {
362        let parser = MTParser::new().unwrap();
363        let result = parser
364            .parse_basic_header("F01BANKDEFFAXXX0123456789")
365            .unwrap();
366
367        if let MessageBlock::BasicHeader {
368            application_id,
369            service_id,
370            logical_terminal,
371            session_number,
372            sequence_number,
373        } = result
374        {
375            assert_eq!(application_id, "F");
376            assert_eq!(service_id, "01");
377            assert_eq!(logical_terminal, "BANKDEFFAXXX");
378            assert_eq!(session_number, "0123");
379            assert_eq!(sequence_number, "456789");
380        } else {
381            panic!("Expected BasicHeader block");
382        }
383    }
384
385    #[test]
386    fn test_application_header_parsing() {
387        let parser = MTParser::new().unwrap();
388        let result = parser
389            .parse_application_header("I103BANKDEFFAXXXU3003")
390            .unwrap();
391
392        if let MessageBlock::ApplicationHeader {
393            input_output_identifier,
394            message_type,
395            destination_address,
396            priority,
397            ..
398        } = result
399        {
400            assert_eq!(input_output_identifier, "I");
401            assert_eq!(message_type, "103");
402            assert_eq!(destination_address, "BANKDEFFAXXX");
403            assert_eq!(priority, "U");
404        } else {
405            panic!("Expected ApplicationHeader block");
406        }
407    }
408
409    #[test]
410    fn test_text_block_parsing() {
411        let parser = MTParser::new().unwrap();
412        let text =
413            ":20:FT21234567890\n:23B:CRED\n:32A:210315EUR1234567,89\n:50K:JOHN DOE\n:59:JANE SMITH";
414        let result = parser.parse_text_block(text).unwrap();
415
416        if let MessageBlock::TextBlock { fields } = result {
417            assert_eq!(fields.len(), 5);
418            assert_eq!(fields[0].tag.as_str(), "20");
419            assert_eq!(fields[0].value(), "FT21234567890");
420            assert_eq!(fields[1].tag.as_str(), "23B");
421            assert_eq!(fields[1].value(), "CRED");
422        } else {
423            panic!("Expected TextBlock");
424        }
425    }
426}