1use crate::errors::{ParseError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MandatoryFieldsConfig {
15 pub message_types: HashMap<String, Vec<String>>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FieldValidationConfig {
22 pub fields: HashMap<String, FieldValidationRule>,
24 pub patterns: HashMap<String, ValidationPattern>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct FieldValidationRule {
31 pub description: String,
33 pub max_length: Option<usize>,
35 pub exact_length: Option<usize>,
37 pub min_length: Option<usize>,
39 pub pattern: Option<String>,
41 pub pattern_ref: Option<String>,
43 pub allow_empty: Option<bool>,
45 pub max_lines: Option<usize>,
47 pub max_chars_per_line: Option<usize>,
49 pub bic_validation: Option<bool>,
51 pub account_validation: Option<bool>,
53 pub case_normalization: Option<String>,
55 pub valid_values: Option<Vec<String>>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ValidationPattern {
62 pub name: String,
64 pub description: String,
66 pub regex: Option<String>,
68 pub charset: Option<CharsetValidation>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, Default)]
74pub struct CharsetValidation {
75 pub alphabetic: Option<bool>,
77 pub numeric: Option<bool>,
79 pub alphanumeric: Option<bool>,
81 pub ascii_printable: Option<bool>,
83 pub allowed_chars: Option<String>,
85 pub forbidden_chars: Option<String>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SwiftConfig {
92 pub mandatory_fields: MandatoryFieldsConfig,
94 pub field_validations: FieldValidationConfig,
96}
97
98pub struct ConfigLoader {
100 config: SwiftConfig,
101}
102
103impl ConfigLoader {
104 pub fn load_from_directory<P: AsRef<Path>>(config_dir: P) -> Result<Self> {
106 let config_dir = config_dir.as_ref();
107
108 let mandatory_fields_path = config_dir.join("mandatory_fields.json");
110 let mandatory_fields = if mandatory_fields_path.exists() {
111 let content =
112 fs::read_to_string(&mandatory_fields_path).map_err(|e| ParseError::IoError {
113 message: e.to_string(),
114 })?;
115 serde_json::from_str(&content)?
116 } else {
117 Self::default_mandatory_fields_config()
119 };
120
121 let validations_path = config_dir.join("field_validations.json");
123 let field_validations = if validations_path.exists() {
124 let content =
125 fs::read_to_string(&validations_path).map_err(|e| ParseError::IoError {
126 message: e.to_string(),
127 })?;
128 serde_json::from_str(&content)?
129 } else {
130 Self::default_field_validations_config()
132 };
133
134 Ok(ConfigLoader {
135 config: SwiftConfig {
136 mandatory_fields,
137 field_validations,
138 },
139 })
140 }
141
142 pub fn load_defaults() -> Self {
144 ConfigLoader {
145 config: SwiftConfig {
146 mandatory_fields: Self::default_mandatory_fields_config(),
147 field_validations: Self::default_field_validations_config(),
148 },
149 }
150 }
151
152 pub fn config(&self) -> &SwiftConfig {
154 &self.config
155 }
156
157 pub fn is_field_mandatory(&self, field_tag: &str, message_type: &str) -> bool {
159 if let Some(mandatory_fields) = self.config.mandatory_fields.message_types.get(message_type)
160 {
161 if mandatory_fields.contains(&field_tag.to_string()) {
163 return true;
164 }
165
166 if field_tag.len() > 2 {
169 let chars: Vec<char> = field_tag.chars().collect();
170 let is_option_pattern = chars.len() == 3
172 && chars[0].is_ascii_digit()
173 && chars[1].is_ascii_digit()
174 && chars[2].is_alphabetic();
175
176 if is_option_pattern {
177 let base_tag = &field_tag[..2];
178 return mandatory_fields.contains(&base_tag.to_string());
179 }
180 }
181
182 false
183 } else {
184 false
185 }
186 }
187
188 pub fn get_field_validation(&self, field_tag: &str) -> Option<&FieldValidationRule> {
190 self.config.field_validations.fields.get(field_tag)
191 }
192
193 pub fn get_validation_pattern(&self, pattern_name: &str) -> Option<&ValidationPattern> {
195 self.config.field_validations.patterns.get(pattern_name)
196 }
197
198 pub fn get_mandatory_fields(&self, message_type: &str) -> Vec<String> {
200 self.config
201 .mandatory_fields
202 .message_types
203 .get(message_type)
204 .cloned()
205 .unwrap_or_default()
206 }
207
208 fn default_mandatory_fields_config() -> MandatoryFieldsConfig {
210 let mut message_types = HashMap::new();
211
212 message_types.insert(
214 "103".to_string(),
215 vec![
216 "20".to_string(),
217 "23B".to_string(),
218 "32A".to_string(),
219 "50".to_string(),
220 "59".to_string(),
221 "71A".to_string(),
222 ],
223 );
224
225 message_types.insert(
227 "102".to_string(),
228 vec![
229 "20".to_string(),
230 "23B".to_string(),
231 "32A".to_string(),
232 "50".to_string(),
233 "71A".to_string(),
234 ],
235 );
236
237 message_types.insert(
239 "202".to_string(),
240 vec![
241 "20".to_string(),
242 "32A".to_string(),
243 "52A".to_string(),
244 "58A".to_string(),
245 ],
246 );
247
248 message_types.insert("199".to_string(), vec!["20".to_string(), "79".to_string()]);
250
251 message_types.insert("192".to_string(), vec!["20".to_string(), "21".to_string()]);
253
254 message_types.insert("195".to_string(), vec!["20".to_string(), "21".to_string()]);
256
257 message_types.insert("196".to_string(), vec!["20".to_string(), "21".to_string()]);
259
260 message_types.insert("197".to_string(), vec!["20".to_string(), "21".to_string()]);
262
263 message_types.insert(
265 "940".to_string(),
266 vec![
267 "20".to_string(),
268 "25".to_string(),
269 "28C".to_string(),
270 "60F".to_string(),
271 "62F".to_string(),
272 ],
273 );
274
275 message_types.insert(
277 "941".to_string(),
278 vec!["20".to_string(), "25".to_string(), "28C".to_string()],
279 );
280
281 message_types.insert(
283 "942".to_string(),
284 vec![
285 "20".to_string(),
286 "25".to_string(),
287 "28C".to_string(),
288 "34F".to_string(),
289 ],
290 );
291
292 MandatoryFieldsConfig { message_types }
293 }
294
295 fn default_field_validations_config() -> FieldValidationConfig {
297 let mut fields = HashMap::new();
298 let mut patterns = HashMap::new();
299
300 patterns.insert(
302 "bic".to_string(),
303 ValidationPattern {
304 name: "bic".to_string(),
305 description: "Bank Identifier Code format".to_string(),
306 regex: Some(r"^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$".to_string()),
307 charset: Some(CharsetValidation {
308 alphanumeric: Some(true),
309 ascii_printable: Some(true),
310 ..Default::default()
311 }),
312 },
313 );
314
315 patterns.insert(
316 "ascii_printable".to_string(),
317 ValidationPattern {
318 name: "ascii_printable".to_string(),
319 description: "ASCII printable characters".to_string(),
320 regex: None,
321 charset: Some(CharsetValidation {
322 ascii_printable: Some(true),
323 ..Default::default()
324 }),
325 },
326 );
327
328 patterns.insert(
329 "alphanumeric".to_string(),
330 ValidationPattern {
331 name: "alphanumeric".to_string(),
332 description: "Alphanumeric characters only".to_string(),
333 regex: None,
334 charset: Some(CharsetValidation {
335 alphanumeric: Some(true),
336 ..Default::default()
337 }),
338 },
339 );
340
341 patterns.insert(
342 "alphabetic".to_string(),
343 ValidationPattern {
344 name: "alphabetic".to_string(),
345 description: "Alphabetic characters only".to_string(),
346 regex: None,
347 charset: Some(CharsetValidation {
348 alphabetic: Some(true),
349 ..Default::default()
350 }),
351 },
352 );
353
354 fields.insert(
356 "20".to_string(),
357 FieldValidationRule {
358 description: "Transaction Reference Number".to_string(),
359 max_length: Some(16),
360 pattern_ref: Some("ascii_printable".to_string()),
361 allow_empty: Some(false),
362 ..Default::default()
363 },
364 );
365
366 fields.insert(
367 "23B".to_string(),
368 FieldValidationRule {
369 description: "Bank Operation Code".to_string(),
370 exact_length: Some(4),
371 pattern_ref: Some("alphanumeric".to_string()),
372 allow_empty: Some(false),
373 case_normalization: Some("upper".to_string()),
374 ..Default::default()
375 },
376 );
377
378 fields.insert(
379 "32A".to_string(),
380 FieldValidationRule {
381 description: "Value Date/Currency/Amount".to_string(),
382 min_length: Some(9),
383 allow_empty: Some(false),
384 ..Default::default()
385 },
386 );
387
388 fields.insert(
389 "50".to_string(),
390 FieldValidationRule {
391 description: "Ordering Customer".to_string(),
392 max_lines: Some(4),
393 max_chars_per_line: Some(35),
394 pattern_ref: Some("ascii_printable".to_string()),
395 allow_empty: Some(false),
396 account_validation: Some(true),
397 ..Default::default()
398 },
399 );
400
401 fields.insert(
402 "52".to_string(),
403 FieldValidationRule {
404 description: "Ordering Institution".to_string(),
405 max_lines: Some(4),
406 max_chars_per_line: Some(35),
407 pattern_ref: Some("ascii_printable".to_string()),
408 allow_empty: Some(false),
409 bic_validation: Some(true),
410 account_validation: Some(true),
411 ..Default::default()
412 },
413 );
414
415 fields.insert(
416 "59".to_string(),
417 FieldValidationRule {
418 description: "Beneficiary Customer".to_string(),
419 max_lines: Some(4),
420 max_chars_per_line: Some(35),
421 pattern_ref: Some("ascii_printable".to_string()),
422 allow_empty: Some(false),
423 account_validation: Some(true),
424 ..Default::default()
425 },
426 );
427
428 fields.insert(
429 "70".to_string(),
430 FieldValidationRule {
431 description: "Remittance Information".to_string(),
432 max_lines: Some(4),
433 max_chars_per_line: Some(35),
434 pattern_ref: Some("ascii_printable".to_string()),
435 allow_empty: Some(true),
436 ..Default::default()
437 },
438 );
439
440 fields.insert(
441 "71A".to_string(),
442 FieldValidationRule {
443 description: "Details of Charges".to_string(),
444 exact_length: Some(3),
445 pattern_ref: Some("alphabetic".to_string()),
446 allow_empty: Some(false),
447 case_normalization: Some("upper".to_string()),
448 valid_values: Some(vec![
449 "BEN".to_string(),
450 "OUR".to_string(),
451 "SHA".to_string(),
452 ]),
453 ..Default::default()
454 },
455 );
456
457 fields.insert(
458 "72".to_string(),
459 FieldValidationRule {
460 description: "Sender to Receiver Information".to_string(),
461 max_lines: Some(6),
462 max_chars_per_line: Some(35),
463 pattern_ref: Some("ascii_printable".to_string()),
464 allow_empty: Some(true),
465 ..Default::default()
466 },
467 );
468
469 FieldValidationConfig { fields, patterns }
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_default_config_loading() {
479 let loader = ConfigLoader::load_defaults();
480
481 assert!(loader.is_field_mandatory("20", "103"));
483 assert!(loader.is_field_mandatory("23B", "103"));
484 assert!(!loader.is_field_mandatory("72", "103"));
485
486 assert!(loader.is_field_mandatory("50A", "103"));
488 assert!(loader.is_field_mandatory("50K", "103"));
489
490 let field_20_validation = loader.get_field_validation("20");
492 assert!(field_20_validation.is_some());
493 assert_eq!(field_20_validation.unwrap().max_length, Some(16));
494
495 let ascii_pattern = loader.get_validation_pattern("ascii_printable");
497 assert!(ascii_pattern.is_some());
498 }
499
500 #[test]
501 fn test_get_mandatory_fields() {
502 let loader = ConfigLoader::load_defaults();
503 let mt103_fields = loader.get_mandatory_fields("103");
504
505 assert!(mt103_fields.contains(&"20".to_string()));
506 assert!(mt103_fields.contains(&"23B".to_string()));
507 assert!(mt103_fields.contains(&"32A".to_string()));
508 }
509}