1use crate::config::{ConfigLoader, FieldValidationRule, ValidationPattern};
7use crate::errors::{FieldParseError, Result};
8use std::sync::OnceLock;
9
10pub mod bic {
12 use super::*;
13
14 pub fn validate_bic(bic: &str) -> Result<()> {
16 if bic.is_empty() {
17 return Err(FieldParseError::invalid_format("BIC", "BIC cannot be empty").into());
18 }
19
20 if bic.len() != 8 && bic.len() != 11 {
22 return Err(FieldParseError::invalid_format(
23 "BIC",
24 "BIC must be 8 or 11 characters long",
25 )
26 .into());
27 }
28
29 let institution_code = &bic[0..4];
31 if !institution_code
32 .chars()
33 .all(|c| c.is_alphabetic() && c.is_ascii())
34 {
35 return Err(FieldParseError::invalid_format(
36 "BIC",
37 "Institution code (first 4 characters) must be alphabetic",
38 )
39 .into());
40 }
41
42 let country_code = &bic[4..6];
44 if !country_code
45 .chars()
46 .all(|c| c.is_alphabetic() && c.is_ascii())
47 {
48 return Err(FieldParseError::invalid_format(
49 "BIC",
50 "Country code (characters 5-6) must be alphabetic",
51 )
52 .into());
53 }
54
55 if !is_valid_country_code(country_code) {
57 return Err(FieldParseError::invalid_format(
58 "BIC",
59 &format!("Invalid country code: {}", country_code),
60 )
61 .into());
62 }
63
64 let location_code = &bic[6..8];
66 if !location_code
67 .chars()
68 .all(|c| c.is_alphanumeric() && c.is_ascii())
69 {
70 return Err(FieldParseError::invalid_format(
71 "BIC",
72 "Location code (characters 7-8) must be alphanumeric",
73 )
74 .into());
75 }
76
77 if bic.len() == 11 {
79 let branch_code = &bic[8..11];
80 if !branch_code
81 .chars()
82 .all(|c| c.is_alphanumeric() && c.is_ascii())
83 {
84 return Err(FieldParseError::invalid_format(
85 "BIC",
86 "Branch code (characters 9-11) must be alphanumeric",
87 )
88 .into());
89 }
90 }
91
92 Ok(())
93 }
94
95 fn is_valid_country_code(code: &str) -> bool {
97 const VALID_CODES: &[&str] = &[
100 "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW",
101 "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN",
102 "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG",
103 "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ",
104 "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI",
105 "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL",
106 "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR",
107 "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM",
108 "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA",
109 "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME",
110 "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU",
111 "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP",
112 "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR",
113 "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD",
114 "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV",
115 "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO",
116 "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE",
117 "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
118 ];
119 VALID_CODES.contains(&code)
120 }
121}
122
123pub mod account {
125 use super::*;
126
127 pub fn validate_account_line_indicator(indicator: &str) -> Result<()> {
129 if indicator.len() != 1 {
130 return Err(FieldParseError::invalid_format(
131 "Account Line Indicator",
132 "Must be exactly 1 character",
133 )
134 .into());
135 }
136
137 let ch = indicator.chars().next().unwrap();
138 if !ch.is_alphanumeric() || !ch.is_ascii() {
139 return Err(FieldParseError::invalid_format(
140 "Account Line Indicator",
141 "Must be alphanumeric ASCII character",
142 )
143 .into());
144 }
145
146 Ok(())
147 }
148
149 pub fn validate_account_number(account: &str) -> Result<()> {
151 if account.is_empty() {
152 return Err(
153 FieldParseError::missing_data("Account", "Account number cannot be empty").into(),
154 );
155 }
156
157 if account.len() > 34 {
158 return Err(FieldParseError::invalid_format(
159 "Account",
160 "Account number cannot exceed 34 characters",
161 )
162 .into());
163 }
164
165 if !account.chars().all(|c| c.is_ascii() && !c.is_control()) {
166 return Err(FieldParseError::invalid_format(
167 "Account",
168 "Account number contains invalid characters",
169 )
170 .into());
171 }
172
173 Ok(())
174 }
175
176 pub fn parse_account_line_and_content(
179 content: &str,
180 ) -> Result<(Option<String>, Option<String>, String)> {
181 if content.is_empty() {
182 return Ok((None, None, content.to_string()));
183 }
184
185 let lines: Vec<&str> = content.lines().collect();
186 if lines.is_empty() {
187 return Ok((None, None, content.to_string()));
188 }
189
190 let first_line = lines[0];
191 let remaining_content = if lines.len() > 1 {
192 lines[1..].join("\n")
193 } else {
194 String::new()
195 };
196
197 if !first_line.starts_with('/') {
199 return Ok((None, None, content.to_string()));
200 }
201
202 let account_part = &first_line[1..]; let parts: Vec<&str> = account_part.split('/').collect();
205
206 match parts.len() {
207 1 => {
208 if parts[0].len() == 1 {
210 Ok((Some(parts[0].to_string()), None, remaining_content))
212 } else {
213 Ok((None, Some(parts[0].to_string()), remaining_content))
215 }
216 }
217 2 => {
218 Ok((
220 Some(parts[0].to_string()),
221 Some(parts[1].to_string()),
222 remaining_content,
223 ))
224 }
225 _ => Err(FieldParseError::invalid_format(
226 "Account Line",
227 "Invalid account line format",
228 )
229 .into()),
230 }
231 }
232}
233
234pub mod multiline {
236 use super::*;
237
238 pub fn validate_multiline_field(
240 field_tag: &str,
241 lines: &[String],
242 max_lines: usize,
243 max_chars_per_line: usize,
244 ) -> Result<()> {
245 if lines.is_empty() {
246 return Err(FieldParseError::missing_data(field_tag, "Content cannot be empty").into());
247 }
248
249 if lines.len() > max_lines {
250 return Err(FieldParseError::invalid_format(
251 field_tag,
252 &format!("Too many lines (max {})", max_lines),
253 )
254 .into());
255 }
256
257 for (i, line) in lines.iter().enumerate() {
258 if line.len() > max_chars_per_line {
259 return Err(FieldParseError::invalid_format(
260 field_tag,
261 &format!("Line {} exceeds {} characters", i + 1, max_chars_per_line),
262 )
263 .into());
264 }
265
266 if line.trim().is_empty() {
267 return Err(FieldParseError::invalid_format(
268 field_tag,
269 &format!("Line {} cannot be empty", i + 1),
270 )
271 .into());
272 }
273
274 if !line.chars().all(|c| c.is_ascii() && !c.is_control()) {
275 return Err(FieldParseError::invalid_format(
276 field_tag,
277 &format!("Line {} contains invalid characters", i + 1),
278 )
279 .into());
280 }
281 }
282
283 Ok(())
284 }
285
286 pub fn parse_lines(content: &str) -> Vec<String> {
288 content
289 .lines()
290 .map(|line| line.trim().to_string())
291 .filter(|line| !line.is_empty())
292 .collect()
293 }
294}
295
296pub mod character {
298 use super::*;
299
300 pub fn validate_ascii_printable(
302 field_tag: &str,
303 content: &str,
304 description: &str,
305 ) -> Result<()> {
306 if !content.chars().all(|c| c.is_ascii() && !c.is_control()) {
307 return Err(FieldParseError::invalid_format(
308 field_tag,
309 &format!("{} contains invalid characters", description),
310 )
311 .into());
312 }
313 Ok(())
314 }
315
316 pub fn validate_alphanumeric(field_tag: &str, content: &str, description: &str) -> Result<()> {
318 if !content.chars().all(|c| c.is_alphanumeric() && c.is_ascii()) {
319 return Err(FieldParseError::invalid_format(
320 field_tag,
321 &format!("{} must contain only alphanumeric characters", description),
322 )
323 .into());
324 }
325 Ok(())
326 }
327
328 pub fn validate_alphabetic(field_tag: &str, content: &str, description: &str) -> Result<()> {
330 if !content.chars().all(|c| c.is_alphabetic() && c.is_ascii()) {
331 return Err(FieldParseError::invalid_format(
332 field_tag,
333 &format!("{} must contain only alphabetic characters", description),
334 )
335 .into());
336 }
337 Ok(())
338 }
339
340 pub fn validate_exact_length(
342 field_tag: &str,
343 content: &str,
344 expected_length: usize,
345 _description: &str,
346 ) -> Result<()> {
347 if content.len() != expected_length {
348 return Err(FieldParseError::InvalidLength {
349 field: field_tag.to_string(),
350 max_length: expected_length,
351 actual_length: content.len(),
352 }
353 .into());
354 }
355 Ok(())
356 }
357
358 pub fn validate_max_length(
360 field_tag: &str,
361 content: &str,
362 max_length: usize,
363 description: &str,
364 ) -> Result<()> {
365 if content.len() > max_length {
366 return Err(FieldParseError::invalid_format(
367 field_tag,
368 &format!("{} exceeds {} characters", description, max_length),
369 )
370 .into());
371 }
372 Ok(())
373 }
374}
375
376static CONFIG_LOADER: OnceLock<ConfigLoader> = OnceLock::new();
378
379pub fn get_config() -> &'static ConfigLoader {
381 CONFIG_LOADER.get_or_init(|| {
382 match ConfigLoader::load_from_directory("config") {
384 Ok(loader) => loader,
385 Err(_) => {
386 ConfigLoader::load_defaults()
388 }
389 }
390 })
391}
392
393pub mod validation {
395 use super::*;
396
397 pub fn validate_field_with_config(field_tag: &str, content: &str) -> Result<()> {
399 let config = get_config();
400
401 let base_tag = if field_tag.len() > 2 {
403 let chars: Vec<char> = field_tag.chars().collect();
404 if chars.len() == 3
405 && chars[0].is_ascii_digit()
406 && chars[1].is_ascii_digit()
407 && chars[2].is_alphabetic()
408 {
409 &field_tag[..2]
410 } else {
411 field_tag
412 }
413 } else {
414 field_tag
415 };
416
417 let rule = config.get_field_validation(field_tag).or_else(|| {
419 if base_tag != field_tag {
420 config.get_field_validation(base_tag)
421 } else {
422 None
423 }
424 });
425
426 if let Some(rule) = rule {
427 validate_with_rule(field_tag, content, rule, config)?;
428 }
429
430 Ok(())
431 }
432
433 pub fn validate_with_rule(
435 field_tag: &str,
436 content: &str,
437 rule: &FieldValidationRule,
438 config: &ConfigLoader,
439 ) -> Result<()> {
440 if content.is_empty() && rule.allow_empty == Some(false) {
442 return Err(FieldParseError::missing_data(field_tag, "Field cannot be empty").into());
443 }
444
445 if let Some(max_length) = rule.max_length {
447 if content.len() > max_length {
448 return Err(
449 FieldParseError::invalid_length(field_tag, max_length, content.len()).into(),
450 );
451 }
452 }
453
454 if let Some(exact_length) = rule.exact_length {
455 if content.len() != exact_length {
456 return Err(FieldParseError::invalid_format(
457 field_tag,
458 &format!("Must be exactly {} characters", exact_length),
459 )
460 .into());
461 }
462 }
463
464 if let Some(min_length) = rule.min_length {
465 if content.len() < min_length {
466 return Err(FieldParseError::invalid_format(
467 field_tag,
468 &format!("Must be at least {} characters", min_length),
469 )
470 .into());
471 }
472 }
473
474 if let Some(pattern_ref) = &rule.pattern_ref {
476 if let Some(pattern) = config.get_validation_pattern(pattern_ref) {
477 validate_with_pattern(field_tag, content, pattern)?;
478 }
479 }
480
481 if let Some(valid_values) = &rule.valid_values {
483 let normalized_content = match rule.case_normalization.as_deref() {
484 Some("upper") => content.to_uppercase(),
485 Some("lower") => content.to_lowercase(),
486 _ => content.to_string(),
487 };
488
489 if !valid_values.contains(&normalized_content) {
490 return Err(FieldParseError::invalid_format(
491 field_tag,
492 &format!("Must be one of: {:?}", valid_values),
493 )
494 .into());
495 }
496 }
497
498 if rule.max_lines.is_some() || rule.max_chars_per_line.is_some() {
500 let lines: Vec<&str> = content.lines().collect();
501
502 if let Some(max_lines) = rule.max_lines {
503 if lines.len() > max_lines {
504 return Err(FieldParseError::invalid_format(
505 field_tag,
506 &format!("Too many lines: {} (max {})", lines.len(), max_lines),
507 )
508 .into());
509 }
510 }
511
512 if let Some(max_chars) = rule.max_chars_per_line {
513 for (i, line) in lines.iter().enumerate() {
514 if line.len() > max_chars {
515 return Err(FieldParseError::invalid_format(
516 field_tag,
517 &format!(
518 "Line {} too long: {} chars (max {})",
519 i + 1,
520 line.len(),
521 max_chars
522 ),
523 )
524 .into());
525 }
526 }
527 }
528 }
529
530 Ok(())
531 }
532
533 pub fn validate_with_pattern(
535 field_tag: &str,
536 content: &str,
537 pattern: &ValidationPattern,
538 ) -> Result<()> {
539 if let Some(regex_str) = &pattern.regex {
541 match regex::Regex::new(regex_str) {
542 Ok(regex) => {
543 if !regex.is_match(content) {
544 return Err(FieldParseError::invalid_format(
545 field_tag,
546 &format!("Does not match pattern: {}", pattern.description),
547 )
548 .into());
549 }
550 }
551 Err(e) => {
552 return Err(FieldParseError::invalid_format(
553 field_tag,
554 &format!("Invalid regex pattern: {}", e),
555 )
556 .into());
557 }
558 }
559 }
560
561 if let Some(charset) = &pattern.charset {
563 if charset.ascii_printable == Some(true) {
564 super::character::validate_ascii_printable(
565 field_tag,
566 content,
567 &pattern.description,
568 )?;
569 }
570 if charset.alphanumeric == Some(true) {
571 super::character::validate_alphanumeric(field_tag, content, &pattern.description)?;
572 }
573 if charset.alphabetic == Some(true) {
574 super::character::validate_alphabetic(field_tag, content, &pattern.description)?;
575 }
576 if charset.numeric == Some(true) && !content.chars().all(|c| c.is_ascii_digit()) {
577 return Err(FieldParseError::invalid_format(
578 field_tag,
579 "Must contain only numeric characters",
580 )
581 .into());
582 }
583 }
584
585 Ok(())
586 }
587}
588
589pub fn is_field_mandatory(field_tag: &str, message_type: &str) -> bool {
591 get_config().is_field_mandatory(field_tag, message_type)
592}
593
594pub fn get_mandatory_fields(message_type: &str) -> Vec<String> {
596 get_config().get_mandatory_fields(message_type)
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 mod bic_tests {
604 use super::*;
605
606 #[test]
607 fn test_valid_bic_8_chars() {
608 assert!(bic::validate_bic("BANKDEFF").is_ok());
609 }
610
611 #[test]
612 fn test_valid_bic_11_chars() {
613 assert!(bic::validate_bic("BANKDEFFXXX").is_ok());
614 }
615
616 #[test]
617 fn test_invalid_bic_length() {
618 assert!(bic::validate_bic("BANK").is_err());
619 assert!(bic::validate_bic("BANKDEFFTOOLONG").is_err());
620 }
621
622 #[test]
623 fn test_invalid_bic_characters() {
624 assert!(bic::validate_bic("BAN1DEFF").is_err()); assert!(bic::validate_bic("BANKD3FF").is_err()); }
627 }
628
629 mod account_tests {
630 use super::*;
631
632 #[test]
633 fn test_valid_account_line_indicator() {
634 assert!(account::validate_account_line_indicator("A").is_ok());
635 assert!(account::validate_account_line_indicator("1").is_ok());
636 }
637
638 #[test]
639 fn test_invalid_account_line_indicator() {
640 assert!(account::validate_account_line_indicator("").is_err());
641 assert!(account::validate_account_line_indicator("AB").is_err());
642 assert!(account::validate_account_line_indicator("@").is_err());
643 }
644
645 #[test]
646 fn test_parse_account_line() {
647 let (indicator, account, content) =
648 account::parse_account_line_and_content("/A/12345\nBANKDEFF").unwrap();
649 assert_eq!(indicator, Some("A".to_string()));
650 assert_eq!(account, Some("12345".to_string()));
651 assert_eq!(content, "BANKDEFF");
652 }
653 }
654
655 mod config_tests {
656 use super::*;
657
658 #[test]
659 fn test_config_based_mandatory_fields() {
660 assert!(is_field_mandatory("20", "103"));
661 assert!(is_field_mandatory("50A", "103")); assert!(is_field_mandatory("50K", "103")); assert!(!is_field_mandatory("72", "103")); assert!(!is_field_mandatory("20", "999")); }
666
667 #[test]
668 fn test_config_based_validation() {
669 assert!(validation::validate_field_with_config("20", "TESTREF123").is_ok());
671
672 assert!(
674 validation::validate_field_with_config("20", "TESTREF123456789012345").is_err()
675 );
676
677 assert!(validation::validate_field_with_config("23B", "CRED").is_ok());
679 assert!(validation::validate_field_with_config("23B", "CRE").is_err()); }
681 }
682}