swift_mt_message/
config.rs

1//! Configuration management for SWIFT MT message processing
2//!
3//! This module handles loading and managing configuration from JSON files,
4//! including mandatory field mappings and field validation rules.
5
6use crate::errors::{ParseError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12/// Configuration for mandatory fields per message type
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MandatoryFieldsConfig {
15    /// Map of message type -> list of mandatory field tags
16    pub message_types: HashMap<String, Vec<String>>,
17}
18
19/// Field validation configuration
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct FieldValidationConfig {
22    /// Field-specific validation rules
23    pub fields: HashMap<String, FieldValidationRule>,
24    /// Common validation patterns that can be reused
25    pub patterns: HashMap<String, ValidationPattern>,
26}
27
28/// Validation rule for a specific field
29#[derive(Debug, Clone, Serialize, Deserialize, Default)]
30pub struct FieldValidationRule {
31    /// Field description
32    pub description: String,
33    /// Maximum length (optional)
34    pub max_length: Option<usize>,
35    /// Exact length (optional)
36    pub exact_length: Option<usize>,
37    /// Minimum length (optional)
38    pub min_length: Option<usize>,
39    /// Character validation pattern
40    pub pattern: Option<String>,
41    /// Custom validation pattern reference
42    pub pattern_ref: Option<String>,
43    /// Whether empty values are allowed
44    pub allow_empty: Option<bool>,
45    /// Maximum number of lines for multi-line fields
46    pub max_lines: Option<usize>,
47    /// Maximum characters per line for multi-line fields
48    pub max_chars_per_line: Option<usize>,
49    /// BIC validation required
50    pub bic_validation: Option<bool>,
51    /// Account validation required
52    pub account_validation: Option<bool>,
53    /// Case normalization (upper, lower, none)
54    pub case_normalization: Option<String>,
55    /// Valid values list (for enumerated fields)
56    pub valid_values: Option<Vec<String>>,
57}
58
59/// Reusable validation pattern
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ValidationPattern {
62    /// Pattern name
63    pub name: String,
64    /// Pattern description
65    pub description: String,
66    /// Regex pattern
67    pub regex: Option<String>,
68    /// Character set validation
69    pub charset: Option<CharsetValidation>,
70}
71
72/// Character set validation options
73#[derive(Debug, Clone, Serialize, Deserialize, Default)]
74pub struct CharsetValidation {
75    /// Allow alphabetic characters
76    pub alphabetic: Option<bool>,
77    /// Allow numeric characters
78    pub numeric: Option<bool>,
79    /// Allow alphanumeric characters
80    pub alphanumeric: Option<bool>,
81    /// Allow ASCII printable characters
82    pub ascii_printable: Option<bool>,
83    /// Specific allowed characters
84    pub allowed_chars: Option<String>,
85    /// Specific forbidden characters
86    pub forbidden_chars: Option<String>,
87}
88
89/// Main configuration container
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SwiftConfig {
92    /// Mandatory fields configuration
93    pub mandatory_fields: MandatoryFieldsConfig,
94    /// Field validation configuration
95    pub field_validations: FieldValidationConfig,
96}
97
98/// Configuration loader
99pub struct ConfigLoader {
100    config: SwiftConfig,
101}
102
103impl ConfigLoader {
104    /// Load configuration from JSON files
105    pub fn load_from_directory<P: AsRef<Path>>(config_dir: P) -> Result<Self> {
106        let config_dir = config_dir.as_ref();
107
108        // Load mandatory fields
109        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            // Fallback to default configuration
118            Self::default_mandatory_fields_config()
119        };
120
121        // Load field validations
122        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            // Fallback to default configuration
131            Self::default_field_validations_config()
132        };
133
134        Ok(ConfigLoader {
135            config: SwiftConfig {
136                mandatory_fields,
137                field_validations,
138            },
139        })
140    }
141
142    /// Load configuration with defaults (used when config files don't exist)
143    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    /// Get the loaded configuration
153    pub fn config(&self) -> &SwiftConfig {
154        &self.config
155    }
156
157    /// Check if a field is mandatory for a specific message type
158    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            // First check exact match
162            if mandatory_fields.contains(&field_tag.to_string()) {
163                return true;
164            }
165
166            // Then handle field options (e.g., "50A", "50K", "50F" all map to "50")
167            // This should only apply to fields that are purely numeric base + alpha option
168            if field_tag.len() > 2 {
169                let chars: Vec<char> = field_tag.chars().collect();
170                // Check if it's all digits followed by a single alpha (like "50A", "51A")
171                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    /// Get validation rule for a field
189    pub fn get_field_validation(&self, field_tag: &str) -> Option<&FieldValidationRule> {
190        self.config.field_validations.fields.get(field_tag)
191    }
192
193    /// Get validation pattern by name
194    pub fn get_validation_pattern(&self, pattern_name: &str) -> Option<&ValidationPattern> {
195        self.config.field_validations.patterns.get(pattern_name)
196    }
197
198    /// Get all mandatory fields for a message type
199    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    /// Default mandatory fields configuration
209    fn default_mandatory_fields_config() -> MandatoryFieldsConfig {
210        let mut message_types = HashMap::new();
211
212        // MT103: Single Customer Credit Transfer
213        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        // MT102: Multiple Customer Credit Transfer
226        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        // MT202: General Financial Institution Transfer
238        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        // MT199: Free Format Message
249        message_types.insert("199".to_string(), vec!["20".to_string(), "79".to_string()]);
250
251        // MT192: Request for Cancellation
252        message_types.insert("192".to_string(), vec!["20".to_string(), "21".to_string()]);
253
254        // MT195: Queries
255        message_types.insert("195".to_string(), vec!["20".to_string(), "21".to_string()]);
256
257        // MT196: Answers
258        message_types.insert("196".to_string(), vec!["20".to_string(), "21".to_string()]);
259
260        // MT197: Copy of a Message
261        message_types.insert("197".to_string(), vec!["20".to_string(), "21".to_string()]);
262
263        // MT940: Customer Statement Message
264        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        // MT941: Balance Report Message
276        message_types.insert(
277            "941".to_string(),
278            vec!["20".to_string(), "25".to_string(), "28C".to_string()],
279        );
280
281        // MT942: Interim Transaction Report
282        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    /// Default field validations configuration
296    fn default_field_validations_config() -> FieldValidationConfig {
297        let mut fields = HashMap::new();
298        let mut patterns = HashMap::new();
299
300        // Define common patterns
301        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        // Define field validation rules
355        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        // Test mandatory fields
482        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        // Test field options
487        assert!(loader.is_field_mandatory("50A", "103"));
488        assert!(loader.is_field_mandatory("50K", "103"));
489
490        // Test field validation
491        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        // Test patterns
496        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}