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
91/// Type aliases for common sizes
92pub type GenericMultiLine3x35 = GenericMultiLineTextField<3, 35>;
93pub type GenericMultiLine4x35 = GenericMultiLineTextField<4, 35>;
94pub type GenericMultiLine6x35 = GenericMultiLineTextField<6, 35>;
95pub type GenericMultiLine6x65 = GenericMultiLineTextField<6, 65>;
96pub type GenericMultiLine20x35 = GenericMultiLineTextField<20, 35>;
97pub type GenericMultiLine35x50 = GenericMultiLineTextField<35, 50>;
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_generic_field_tag_removal() {
105        // Test various field tag patterns
106        let test_cases = vec![
107            (":50K:/1234567890", "/1234567890"),
108            ("50K:/1234567890", "/1234567890"),
109            (":20:FT123456789", "FT123456789"),
110            ("20:FT123456789", "FT123456789"),
111            (":32A:241231USD1000000,", "241231USD1000000,"),
112            ("32A:241231USD1000000,", "241231USD1000000,"),
113            (":71F:USD50,00", "USD50,00"),
114            ("71F:USD50,00", "USD50,00"),
115            (":13C:/123045+0/+0100/-0500", "/123045+0/+0100/-0500"),
116            ("13C:/123045+0/+0100/-0500", "/123045+0/+0100/-0500"),
117            // Edge cases
118            ("plain_text", "plain_text"), // No field tag
119            ("", ""),                     // Empty string
120            (":", ":"),                   // Just colon
121            ("::", "::"),                 // Two colons - no valid field tag pattern
122        ];
123
124        for (input, expected) in test_cases {
125            let result = GenericMultiLineTextField::<4, 35>::remove_field_tag_prefix(input);
126            assert_eq!(result, expected, "Failed for input: '{input}'");
127        }
128
129        println!("✅ Generic field tag removal works for all patterns!");
130    }
131
132    #[test]
133    fn test_multiline_field_parsing() {
134        // Test multiline field with field tag
135        let input = "50K:/1234567890\nACME CORPORATION\n123 MAIN STREET\nNEW YORK NY 10001 US";
136
137        let result = GenericMultiLineTextField::<4, 35>::parse(input).unwrap();
138
139        assert_eq!(result.lines.len(), 4);
140        assert_eq!(result.lines[0], "/1234567890");
141        assert_eq!(result.lines[1], "ACME CORPORATION");
142        assert_eq!(result.lines[2], "123 MAIN STREET");
143        assert_eq!(result.lines[3], "NEW YORK NY 10001 US");
144
145        println!("✅ Multiline field parsing with field tag removal works correctly!");
146    }
147}