swift_mt_message/parser/
message_parser.rs

1//! # Message Parser Module
2//!
3//! Pointer-based parser for SWIFT MT messages that tracks position while parsing fields sequentially.
4//! This replaces the HashMap-based approach with a more efficient single-pass parser.
5
6use crate::errors::{InvalidFieldFormatError, ParseError};
7use crate::traits::SwiftField;
8use std::collections::HashSet;
9
10use super::field_extractor::extract_field_content;
11
12/// Message parser that tracks position while parsing SWIFT messages
13#[derive(Debug)]
14pub struct MessageParser<'a> {
15    /// The input message text
16    input: &'a str,
17    /// Current position in the input
18    position: usize,
19    /// Fields that have been parsed (for duplicate detection)
20    fields_seen: HashSet<String>,
21    /// Message type being parsed
22    message_type: String,
23    /// Whether to allow duplicate fields
24    allow_duplicates: bool,
25}
26
27impl<'a> MessageParser<'a> {
28    /// Create a new message parser
29    pub fn new(input: &'a str, message_type: &str) -> Self {
30        Self {
31            input,
32            position: 0,
33            fields_seen: HashSet::new(),
34            message_type: message_type.to_string(),
35            allow_duplicates: false,
36        }
37    }
38
39    /// Enable or disable duplicate field handling
40    pub fn with_duplicates(mut self, allow: bool) -> Self {
41        self.allow_duplicates = allow;
42        self
43    }
44
45    /// Parse a required field
46    pub fn parse_field<T: SwiftField>(&mut self, tag: &str) -> Result<T, ParseError> {
47        let field_content = self.extract_field(tag, false)?;
48
49        // Try to parse the field
50        T::parse(&field_content).map_err(|e| {
51            ParseError::InvalidFieldFormat(Box::new(InvalidFieldFormatError {
52                field_tag: tag.to_string(),
53                component_name: "field".to_string(),
54                value: field_content,
55                format_spec: "field format".to_string(),
56                position: Some(self.position),
57                inner_error: e.to_string(),
58            }))
59        })
60    }
61
62    /// Parse an optional field (only checks immediate next field, not searching ahead)
63    pub fn parse_optional_field<T: SwiftField>(
64        &mut self,
65        tag: &str,
66    ) -> Result<Option<T>, ParseError> {
67        // For optional fields, only check if the immediate next field matches
68        // Don't search ahead in the input to avoid consuming fields from later sections
69        if !self.detect_field(tag) {
70            return Ok(None);
71        }
72
73        // If immediate next field matches, extract and parse it
74        match self.extract_field(tag, true) {
75            Ok(content) => {
76                let parsed = T::parse(&content).map_err(|e| {
77                    ParseError::InvalidFieldFormat(Box::new(InvalidFieldFormatError {
78                        field_tag: tag.to_string(),
79                        component_name: "field".to_string(),
80                        value: content,
81                        format_spec: "field format".to_string(),
82                        position: Some(self.position),
83                        inner_error: e.to_string(),
84                    }))
85                })?;
86                Ok(Some(parsed))
87            }
88            Err(_) => Ok(None), // Field not found, return None for optional
89        }
90    }
91
92    /// Parse a repeated field (returns Vec)
93    pub fn parse_repeated_field<T: SwiftField>(&mut self, tag: &str) -> Result<Vec<T>, ParseError> {
94        let mut results = Vec::new();
95
96        // Keep parsing until no more instances found
97        while let Ok(content) = self.extract_field(tag, true) {
98            let parsed = T::parse(&content).map_err(|e| {
99                ParseError::InvalidFieldFormat(Box::new(InvalidFieldFormatError {
100                    field_tag: tag.to_string(),
101                    component_name: "field".to_string(),
102                    value: content,
103                    format_spec: "field format".to_string(),
104                    position: Some(self.position),
105                    inner_error: e.to_string(),
106                }))
107            })?;
108            results.push(parsed);
109        }
110
111        Ok(results)
112    }
113
114    /// Parse a field with variant detection (for enum fields)
115    pub fn parse_variant_field<T: SwiftField>(&mut self, base_tag: &str) -> Result<T, ParseError> {
116        // Look ahead to find which variant is present
117        let variant = self.detect_variant(base_tag)?;
118        let full_tag = format!("{}{}", base_tag, variant);
119        let field_content = self.extract_field(&full_tag, false)?;
120
121        // Use parse_with_variant for enum fields
122        T::parse_with_variant(&field_content, Some(&variant), Some(base_tag)).map_err(|e| {
123            ParseError::InvalidFieldFormat(Box::new(InvalidFieldFormatError {
124                field_tag: full_tag,
125                component_name: "field".to_string(),
126                value: field_content,
127                format_spec: "field format".to_string(),
128                position: Some(self.position),
129                inner_error: e.to_string(),
130            }))
131        })
132    }
133
134    /// Parse an optional field with variant detection
135    pub fn parse_optional_variant_field<T: SwiftField>(
136        &mut self,
137        base_tag: &str,
138    ) -> Result<Option<T>, ParseError> {
139        match self.detect_variant_optional(base_tag) {
140            Some(variant) => {
141                let full_tag = format!("{}{}", base_tag, variant);
142                if let Ok(content) = self.extract_field(&full_tag, true) {
143                    let parsed = T::parse_with_variant(&content, Some(&variant), Some(base_tag))
144                        .map_err(|e| {
145                            ParseError::InvalidFieldFormat(Box::new(InvalidFieldFormatError {
146                                field_tag: full_tag,
147                                component_name: "field".to_string(),
148                                value: content,
149                                format_spec: "field format".to_string(),
150                                position: Some(self.position),
151                                inner_error: e.to_string(),
152                            }))
153                        })?;
154                    Ok(Some(parsed))
155                } else {
156                    Ok(None)
157                }
158            }
159            None => Ok(None), // No variant found
160        }
161    }
162
163    /// Extract field content from the message
164    fn extract_field(&mut self, tag: &str, optional: bool) -> Result<String, ParseError> {
165        // Check for duplicates if not allowed
166        if !self.allow_duplicates && self.fields_seen.contains(tag) && !optional {
167            return Err(ParseError::InvalidFormat {
168                message: format!("Duplicate field: {}", tag),
169            });
170        }
171
172        #[cfg(debug_assertions)]
173        {
174            if tag.starts_with("21")
175                || tag.starts_with("32")
176                || tag.starts_with("57")
177                || tag.starts_with("59")
178            {
179                eprintln!(
180                    "DEBUG extract_field('{}') at position {} (allow_duplicates={})",
181                    tag, self.position, self.allow_duplicates
182                );
183                eprintln!(
184                    "  -> input slice (first 100 chars): {:?}",
185                    &self.input[self.position..]
186                        .chars()
187                        .take(100)
188                        .collect::<String>()
189                );
190            }
191        }
192
193        // Extract field content using the field_extractor module
194        let extract_result = extract_field_content(&self.input[self.position..], tag);
195
196        #[cfg(debug_assertions)]
197        {
198            if tag.starts_with("21")
199                || tag.starts_with("32")
200                || tag.starts_with("57")
201                || tag.starts_with("59")
202            {
203                eprintln!(
204                    "  -> extract_field_content returned: {:?}",
205                    extract_result
206                        .as_ref()
207                        .map(|(c, consumed)| (c.len(), *consumed))
208                );
209            }
210        }
211
212        match extract_result {
213            Some((content, consumed)) => {
214                #[cfg(debug_assertions)]
215                {
216                    if tag.starts_with("21")
217                        || tag.starts_with("32")
218                        || tag.starts_with("57")
219                        || tag.starts_with("59")
220                    {
221                        eprintln!(
222                            "  -> extracted content length={}, consumed={}, new position={}",
223                            content.len(),
224                            consumed,
225                            self.position + consumed
226                        );
227                        eprintln!(
228                            "  -> content (first 40 chars): {:?}",
229                            &content.chars().take(40).collect::<String>()
230                        );
231                    }
232                }
233                self.position += consumed;
234                // Only track fields if duplicates are not allowed
235                if !self.allow_duplicates {
236                    self.fields_seen.insert(tag.to_string());
237                }
238                Ok(content)
239            }
240            None => {
241                #[cfg(debug_assertions)]
242                {
243                    if tag.starts_with("21")
244                        || tag.starts_with("32")
245                        || tag.starts_with("57")
246                        || tag.starts_with("59")
247                    {
248                        eprintln!("  -> NOT FOUND");
249                    }
250                }
251                if optional {
252                    // For optional fields, just return a format error that will be caught
253                    Err(ParseError::InvalidFormat {
254                        message: format!("Optional field {} not found", tag),
255                    })
256                } else {
257                    Err(ParseError::MissingRequiredField {
258                        field_tag: tag.to_string(),
259                        field_name: tag.to_string(),
260                        message_type: self.message_type.clone(),
261                        position_in_block4: Some(self.position),
262                    })
263                }
264            }
265        }
266    }
267
268    /// Detect which variant is present for an enum field
269    fn detect_variant(&self, base_tag: &str) -> Result<String, ParseError> {
270        // Look for common variants in order of preference
271        let common_variants = vec!["A", "B", "C", "D", "F", "K", "L"];
272
273        // Get the remaining input
274        let remaining = &self.input[self.position..];
275
276        // For required fields, we should find it immediately (possibly after whitespace)
277        let trimmed = remaining.trim_start_matches(|c: char| c.is_whitespace());
278
279        #[cfg(debug_assertions)]
280        {
281            if base_tag == "59" {
282                eprintln!(
283                    "DEBUG detect_variant('{}') at position {}",
284                    base_tag, self.position
285                );
286                eprintln!(
287                    "  remaining (first 80 chars): {:?}",
288                    &remaining.chars().take(80).collect::<String>()
289                );
290                eprintln!(
291                    "  trimmed (first 80 chars): {:?}",
292                    &trimmed.chars().take(80).collect::<String>()
293                );
294            }
295        }
296
297        for variant in common_variants {
298            let full_tag = format!("{}{}", base_tag, variant);
299            if trimmed.starts_with(&format!(":{}:", full_tag)) {
300                #[cfg(debug_assertions)]
301                {
302                    if base_tag == "59" {
303                        eprintln!("  -> Found variant '{}'", variant);
304                    }
305                }
306                return Ok(variant.to_string());
307            }
308        }
309
310        // Also check for no-variant version (just the base tag)
311        if trimmed.starts_with(&format!(":{}:", base_tag)) {
312            #[cfg(debug_assertions)]
313            {
314                if base_tag == "59" {
315                    eprintln!("  -> Found no-variant (base tag only)");
316                }
317            }
318            return Ok(String::new());
319        }
320
321        #[cfg(debug_assertions)]
322        {
323            if base_tag == "59" {
324                eprintln!("  -> NOT FOUND - returning error");
325            }
326        }
327
328        Err(ParseError::MissingRequiredField {
329            field_tag: base_tag.to_string(),
330            field_name: base_tag.to_string(),
331            message_type: self.message_type.clone(),
332            position_in_block4: Some(self.position),
333        })
334    }
335
336    /// Detect variant for optional fields
337    pub fn detect_variant_optional(&self, base_tag: &str) -> Option<String> {
338        // Look for common variants
339        let common_variants = vec!["A", "B", "C", "D", "F", "K", "L"];
340
341        // Get the remaining input
342        let remaining = &self.input[self.position..];
343
344        // Skip any leading whitespace
345        let trimmed = remaining.trim_start_matches(|c: char| c.is_whitespace());
346
347        // Check if the immediate next field is one of our variants
348        for variant in common_variants {
349            let full_tag = format!("{}{}", base_tag, variant);
350            if trimmed.starts_with(&format!(":{}:", full_tag)) {
351                return Some(variant.to_string());
352            }
353        }
354
355        // Check for no-variant version
356        if trimmed.starts_with(&format!(":{}:", base_tag)) {
357            return Some(String::new());
358        }
359
360        None
361    }
362
363    /// Get current position in input
364    pub fn position(&self) -> usize {
365        self.position
366    }
367
368    /// Get remaining unparsed content (useful for debugging)
369    pub fn remaining(&self) -> &str {
370        &self.input[self.position..]
371    }
372
373    /// Check if we've reached the end of input
374    pub fn is_complete(&self) -> bool {
375        self.position >= self.input.len()
376            || self.remaining().trim().is_empty()
377            || self.remaining().trim() == "-"
378    }
379
380    /// Check if a field exists in the remaining content
381    pub fn detect_field(&self, tag: &str) -> bool {
382        let remaining = self.remaining();
383        let trimmed = remaining.trim_start_matches(|c: char| c.is_whitespace());
384        trimmed.starts_with(&format!(":{}:", tag))
385    }
386
387    /// Peek at the variant of a field without consuming it
388    /// Returns the variant letter (e.g., "A", "K", "C", "L") if the field exists
389    pub fn peek_field_variant(&self, base_tag: &str) -> Option<String> {
390        let remaining = self.remaining();
391        let trimmed = remaining.trim_start_matches(|c: char| c.is_whitespace());
392
393        // Try to find field with any variant (e.g., :50A:, :50K:, etc.)
394        for variant in [
395            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q',
396            'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
397        ] {
398            let search_pattern = format!(":{}{}:", base_tag, variant);
399            if trimmed.starts_with(&search_pattern) {
400                return Some(variant.to_string());
401            }
402        }
403
404        // Check for field without variant (e.g., :50:)
405        let search_pattern = format!(":{}:", base_tag);
406        if trimmed.starts_with(&search_pattern) {
407            return Some("".to_string()); // No variant
408        }
409
410        None
411    }
412}