swift_mt_message/fields/common/
multiline_text_field.rs

1use crate::{Result, SwiftField, ValidationError, ValidationResult};
2use serde::{Deserialize, Serialize};
3
4/// Generic MultiLine Text Field
5///
6/// Parameterized multiline field for different line and character constraints.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8pub struct GenericMultiLineTextField<const MAX_LINES: usize, const MAX_CHARS: usize> {
9    /// Text lines
10    pub lines: Vec<String>,
11}
12
13impl<const MAX_LINES: usize, const MAX_CHARS: usize>
14    GenericMultiLineTextField<MAX_LINES, MAX_CHARS>
15{
16    /// Remove field tag prefix using generic regex pattern
17    /// Handles patterns like ":50K:", "50K:", ":20:", "32A:", etc.
18    fn remove_field_tag_prefix(value: &str) -> &str {
19        // Use lazy_static for regex compilation performance
20        use std::sync::OnceLock;
21        static FIELD_TAG_REGEX: OnceLock<regex::Regex> = OnceLock::new();
22
23        let regex = FIELD_TAG_REGEX.get_or_init(|| {
24            // Pattern matches: optional colon + field identifier + mandatory colon
25            // Field identifier: 1-3 digits optionally followed by 1-2 letters
26            regex::Regex::new(r"^:?([0-9]{1,3}[A-Z]{0,2}):").unwrap()
27        });
28
29        if let Some(captures) = regex.find(value) {
30            &value[captures.end()..]
31        } else {
32            value
33        }
34    }
35}
36
37impl<const MAX_LINES: usize, const MAX_CHARS: usize> SwiftField
38    for GenericMultiLineTextField<MAX_LINES, MAX_CHARS>
39{
40    fn parse(value: &str) -> Result<Self> {
41        let content = value.trim();
42
43        // Remove field tag prefix using generic regex pattern
44        let content = Self::remove_field_tag_prefix(content);
45
46        let lines = content.lines().map(|line| line.to_string()).collect();
47        Ok(Self { lines })
48    }
49
50    fn to_swift_string(&self) -> String {
51        self.lines.join("\n")
52    }
53
54    fn validate(&self) -> ValidationResult {
55        let mut errors = Vec::new();
56        let warnings = Vec::new();
57
58        if self.lines.len() > MAX_LINES {
59            errors.push(ValidationError::ValueValidation {
60                field_tag: "multiline".to_string(),
61                message: format!("Too many lines: {} (max {})", self.lines.len(), MAX_LINES),
62            });
63        }
64
65        for (i, line) in self.lines.iter().enumerate() {
66            if line.len() > MAX_CHARS {
67                errors.push(ValidationError::ValueValidation {
68                    field_tag: "multiline".to_string(),
69                    message: format!(
70                        "Line {} too long: {} chars (max {})",
71                        i + 1,
72                        line.len(),
73                        MAX_CHARS
74                    ),
75                });
76            }
77        }
78
79        ValidationResult {
80            is_valid: errors.is_empty(),
81            errors,
82            warnings,
83        }
84    }
85
86    fn format_spec() -> &'static str {
87        "lines"
88    }
89
90    fn sample() -> Self {
91        let line_count = if MAX_LINES > 4 { 4 } else { MAX_LINES };
92        let lines = crate::sample::generate_name_and_address(line_count);
93        Self { lines }
94    }
95
96    fn sample_with_config(_config: &crate::sample::FieldConfig) -> Self {
97        Self::sample()
98    }
99}
100
101/// Type aliases for common sizes
102pub type GenericMultiLine3x35 = GenericMultiLineTextField<3, 35>;
103pub type GenericMultiLine4x35 = GenericMultiLineTextField<4, 35>;
104pub type GenericMultiLine6x35 = GenericMultiLineTextField<6, 35>;
105pub type GenericMultiLine6x65 = GenericMultiLineTextField<6, 65>;
106pub type GenericMultiLine20x35 = GenericMultiLineTextField<20, 35>;
107pub type GenericMultiLine35x50 = GenericMultiLineTextField<35, 50>;
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_generic_field_tag_removal() {
115        // Test various field tag patterns
116        let test_cases = vec![
117            (":50K:/1234567890", "/1234567890"),
118            ("50K:/1234567890", "/1234567890"),
119            (":20:FT123456789", "FT123456789"),
120            ("20:FT123456789", "FT123456789"),
121            (":32A:241231USD1000000,", "241231USD1000000,"),
122            ("32A:241231USD1000000,", "241231USD1000000,"),
123            (":71F:USD50,00", "USD50,00"),
124            ("71F:USD50,00", "USD50,00"),
125            (":13C:/123045+0/+0100/-0500", "/123045+0/+0100/-0500"),
126            ("13C:/123045+0/+0100/-0500", "/123045+0/+0100/-0500"),
127            // Edge cases
128            ("plain_text", "plain_text"), // No field tag
129            ("", ""),                     // Empty string
130            (":", ":"),                   // Just colon
131            ("::", "::"),                 // Two colons - no valid field tag pattern
132        ];
133
134        for (input, expected) in test_cases {
135            let result = GenericMultiLineTextField::<4, 35>::remove_field_tag_prefix(input);
136            assert_eq!(result, expected, "Failed for input: '{input}'");
137        }
138
139        println!("✅ Generic field tag removal works for all patterns!");
140    }
141
142    #[test]
143    fn test_multiline_field_parsing() {
144        // Test multiline field with field tag
145        let input = "50K:/1234567890\nACME CORPORATION\n123 MAIN STREET\nNEW YORK NY 10001 US";
146
147        let result = GenericMultiLineTextField::<4, 35>::parse(input).unwrap();
148
149        assert_eq!(result.lines.len(), 4);
150        assert_eq!(result.lines[0], "/1234567890");
151        assert_eq!(result.lines[1], "ACME CORPORATION");
152        assert_eq!(result.lines[2], "123 MAIN STREET");
153        assert_eq!(result.lines[3], "NEW YORK NY 10001 US");
154
155        println!("✅ Multiline field parsing with field tag removal works correctly!");
156    }
157}