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}