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}