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                #[cfg(debug_assertions)]
50                {
51                    if raw_field_tag.starts_with("50") {
52                        eprintln!(
53                            "DEBUG: parse_block4_fields - raw_field_tag='{}', normalized field_tag='{}'",
54                            raw_field_tag, field_tag
55                        );
56                    }
57                }
58
59                // Find the end of field value (next field marker or end of content)
60                let value_start = tag_end + 1;
61                let value_end = if let Some(next_field) = content[value_start..].find("\n:") {
62                    value_start + next_field
63                } else {
64                    content.len()
65                };
66
67                // Avoid unnecessary string allocation - trim inline during push
68                let field_value_slice = &content[value_start..value_end];
69                let trimmed_value = field_value_slice.trim();
70
71                // Store field value with enhanced position info (line number encoded with field position)
72                // High 16 bits: line number, Low 16 bits: field position
73                let position_info = (line_number << 16) | (field_position & 0xFFFF);
74
75                // Add to existing Vec or create new Vec for this field tag
76                field_map
77                    .entry(field_tag.into_owned())
78                    .or_default()
79                    .push((trimmed_value.to_string(), position_info));
80
81                field_position += 1; // Increment position for next field
82                current_pos = value_end;
83            } else {
84                // Last field or malformed - provide detailed error
85                return Err(ParseError::InvalidBlockStructure {
86                    block: "4".to_string(),
87                    message: format!(
88                        "Malformed field tag at line {line_number}, position {current_pos}"
89                    ),
90                });
91            }
92        } else {
93            break;
94        }
95    }
96
97    Ok(field_map)
98}
99
100/// Normalize field tag by removing option letters (A, F, K, etc.)
101/// Example: "50K" -> "50", "59A" -> "59", "20" -> "20"
102/// But preserve option letters for fields that have multiple variants like 23B/23E, 71A/71F/71G
103/// Also preserve numbered field tags like "50#1", "50#2"
104pub fn normalize_field_tag(raw_tag: &str) -> std::borrow::Cow<'_, str> {
105    use std::borrow::Cow;
106
107    // Special handling for numbered field tags (e.g., "50#1", "50#2")
108    if raw_tag.contains('#') {
109        // For numbered fields, we need to keep the full tag including the # and number
110        // This is used in MT101 and other messages to distinguish multiple occurrences
111        return Cow::Borrowed(raw_tag);
112    }
113
114    // Find where the numeric part ends
115    let numeric_end = raw_tag
116        .find(|c: char| !c.is_ascii_digit())
117        .unwrap_or(raw_tag.len());
118
119    // If no suffix, return the tag as-is
120    if numeric_end == raw_tag.len() {
121        return Cow::Borrowed(raw_tag);
122    }
123
124    let numeric_part = &raw_tag[..numeric_end];
125    let suffix = &raw_tag[numeric_end..];
126
127    // For certain field numbers, preserve the option letter to avoid conflicts
128    match numeric_part {
129        "11" | "13" | "21" | "23" | "25" | "26" | "28" | "32" | "33" | "34" | "37" | "50"
130        | "51" | "52" | "53" | "54" | "55" | "56" | "57" | "58" | "59" | "60" | "62" | "71"
131        | "77" | "90" => {
132            // Keep option letters for fields that have multiple variants or specific formats
133            Cow::Borrowed(raw_tag)
134        }
135        _ => {
136            // For other fields, check if suffix is just uppercase letters
137            if suffix.chars().all(|c| c.is_ascii_uppercase()) {
138                // It's an option letter, return just the numeric part
139                Cow::Owned(numeric_part.to_string())
140            } else {
141                // Not a simple option letter, keep the full tag
142                Cow::Borrowed(raw_tag)
143            }
144        }
145    }
146}
147
148/// Extract base tag by removing index suffix (e.g., "50#1" -> "50")
149pub fn extract_base_tag(tag: &str) -> &str {
150    if let Some(index_pos) = tag.find('#') {
151        &tag[..index_pos]
152    } else {
153        tag
154    }
155}