swift_mt_message/parser/
generated.rs

1//! Generated parser functions
2//!
3//! This module contains parser functions that are generated by the swift-mt-message-macros crate.
4//! These functions replace the manual implementations that were previously in parser_impl.rs.
5
6use crate::errors::{ParseError, Result};
7use std::collections::HashMap;
8
9// Include the generated parser functions from the macro crate
10// These will be generated at compile time based on the macro definitions
11
12/// Parse block 4 fields into a field map with enhanced position tracking
13///
14/// This function is generated by the macro system to ensure consistent parsing
15/// across all field types and messages.
16pub fn parse_block4_fields(block4: &str) -> Result<HashMap<String, Vec<(String, usize)>>> {
17    // Pre-allocate HashMap with estimated capacity based on typical field count
18    // Most messages have between 10-60 fields
19    let estimated_fields = block4.matches("\n:").count().max(10);
20    let mut field_map: HashMap<String, Vec<(String, usize)>> =
21        HashMap::with_capacity(estimated_fields);
22
23    // Remove leading/trailing whitespace and newlines
24    let content = block4.trim();
25
26    // Split by field markers (:XX:)
27    let mut current_pos = 0;
28    let mut field_position = 0; // Track sequential position for consumption ordering
29    let mut line_number = 1;
30
31    while current_pos < content.len() {
32        // Track line numbers for better error reporting
33        if current_pos > 0 && content.chars().nth(current_pos - 1) == Some('\n') {
34            line_number += 1;
35        }
36
37        // Find next field marker
38        if let Some(field_start) = content[current_pos..].find(':') {
39            let field_start = current_pos + field_start;
40
41            // Extract field tag (characters after : until next :)
42            if let Some(tag_end) = content[field_start + 1..].find(':') {
43                let tag_end = field_start + 1 + tag_end;
44                let raw_field_tag = &content[field_start + 1..tag_end];
45
46                // Normalize field tag by removing option letters (A, F, K, etc.)
47                let field_tag = normalize_field_tag(raw_field_tag);
48
49                // Find the end of field value (next field marker or end of content)
50                let value_start = tag_end + 1;
51                let value_end = if let Some(next_field) = content[value_start..].find("\n:") {
52                    value_start + next_field
53                } else {
54                    content.len()
55                };
56
57                // Avoid unnecessary string allocation - trim inline during push
58                let field_value_slice = &content[value_start..value_end];
59                let trimmed_value = field_value_slice.trim();
60
61                // Store field value with enhanced position info (line number encoded with field position)
62                // High 16 bits: line number, Low 16 bits: field position
63                let position_info = (line_number << 16) | (field_position & 0xFFFF);
64
65                // Add to existing Vec or create new Vec for this field tag
66                field_map
67                    .entry(field_tag.into_owned())
68                    .or_default()
69                    .push((trimmed_value.to_string(), position_info));
70
71                field_position += 1; // Increment position for next field
72                current_pos = value_end;
73            } else {
74                // Last field or malformed - provide detailed error
75                return Err(ParseError::InvalidBlockStructure {
76                    block: "4".to_string(),
77                    message: format!(
78                        "Malformed field tag at line {line_number}, position {current_pos}"
79                    ),
80                });
81            }
82        } else {
83            break;
84        }
85    }
86
87    Ok(field_map)
88}
89
90/// Normalize field tag by removing option letters (A, F, K, etc.)
91/// Example: "50K" -> "50", "59A" -> "59", "20" -> "20"
92/// But preserve option letters for fields that have multiple variants like 23B/23E, 71A/71F/71G
93/// Also preserve numbered field tags like "50#1", "50#2"
94pub fn normalize_field_tag(raw_tag: &str) -> std::borrow::Cow<'_, str> {
95    use std::borrow::Cow;
96
97    // Special handling for numbered field tags (e.g., "50#1", "50#2")
98    if raw_tag.contains('#') {
99        // For numbered fields, we need to keep the full tag including the # and number
100        // This is used in MT101 and other messages to distinguish multiple occurrences
101        return Cow::Borrowed(raw_tag);
102    }
103
104    // Find where the numeric part ends
105    let numeric_end = raw_tag
106        .find(|c: char| !c.is_ascii_digit())
107        .unwrap_or(raw_tag.len());
108
109    // If no suffix, return the tag as-is
110    if numeric_end == raw_tag.len() {
111        return Cow::Borrowed(raw_tag);
112    }
113
114    let numeric_part = &raw_tag[..numeric_end];
115    let suffix = &raw_tag[numeric_end..];
116
117    // For certain field numbers, preserve the option letter to avoid conflicts
118    match numeric_part {
119        "11" | "13" | "21" | "23" | "25" | "26" | "28" | "32" | "33" | "34" | "37" | "50"
120        | "52" | "53" | "54" | "55" | "56" | "57" | "58" | "59" | "60" | "62" | "71" | "77" => {
121            // Keep option letters for fields that have multiple variants or specific formats
122            Cow::Borrowed(raw_tag)
123        }
124        _ => {
125            // For other fields, check if suffix is just uppercase letters
126            if suffix.chars().all(|c| c.is_ascii_uppercase()) {
127                // It's an option letter, return just the numeric part
128                Cow::Owned(numeric_part.to_string())
129            } else {
130                // Not a simple option letter, keep the full tag
131                Cow::Borrowed(raw_tag)
132            }
133        }
134    }
135}
136
137/// Extract base tag by removing index suffix (e.g., "50#1" -> "50")
138pub fn extract_base_tag(tag: &str) -> &str {
139    if let Some(index_pos) = tag.find('#') {
140        &tag[..index_pos]
141    } else {
142        tag
143    }
144}