swift_mt_message/
scenario_config.rs

1//! Test scenario configuration module for generating SWIFT MT messages
2//!
3//! This module simply loads scenario JSON files and passes them to datafake-rs
4
5use serde_json::Value;
6use std::env;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::errors::{ParseError, Result};
11
12/// Configuration for scenario file paths
13#[derive(Debug, Clone)]
14pub struct ScenarioConfig {
15    /// Base paths to search for scenario files
16    pub base_paths: Vec<PathBuf>,
17}
18
19impl Default for ScenarioConfig {
20    fn default() -> Self {
21        // Check environment variable first
22        if let Ok(env_paths) = env::var("SWIFT_SCENARIO_PATH") {
23            let paths = parse_env_paths(&env_paths);
24            if !paths.is_empty() {
25                return Self { base_paths: paths };
26            }
27        }
28
29        // Default paths
30        Self {
31            base_paths: vec![
32                PathBuf::from("test_scenarios"),
33                PathBuf::from("../test_scenarios"),
34            ],
35        }
36    }
37}
38
39impl ScenarioConfig {
40    /// Create a new configuration with default paths
41    pub fn new() -> Self {
42        Self::default()
43    }
44
45    /// Create a configuration with specific paths
46    pub fn with_paths(paths: Vec<PathBuf>) -> Self {
47        Self { base_paths: paths }
48    }
49
50    /// Add a path to the configuration
51    pub fn add_path(mut self, path: PathBuf) -> Self {
52        self.base_paths.push(path);
53        self
54    }
55
56    /// Clear all paths and set new ones
57    pub fn set_paths(mut self, paths: Vec<PathBuf>) -> Self {
58        self.base_paths = paths;
59        self
60    }
61}
62
63/// Parse environment variable paths
64fn parse_env_paths(env_value: &str) -> Vec<PathBuf> {
65    // Use OS-specific path separator
66    #[cfg(windows)]
67    let separator = ';';
68    #[cfg(not(windows))]
69    let separator = ':';
70
71    env_value
72        .split(separator)
73        .filter(|s| !s.is_empty())
74        .map(PathBuf::from)
75        .collect()
76}
77
78/// Load a scenario configuration from a JSON file
79pub fn load_scenario_json<P: AsRef<Path>>(path: P) -> Result<Value> {
80    let content = fs::read_to_string(path).map_err(|e| ParseError::InvalidFormat {
81        message: format!("Failed to read scenario file: {e}"),
82    })?;
83
84    serde_json::from_str(&content).map_err(|e| ParseError::InvalidFormat {
85        message: format!("Failed to parse scenario JSON: {e}"),
86    })
87}
88
89/// Find and load a scenario for a specific message type with custom configuration
90///
91/// Looks for configuration files in the following order:
92/// 1. {base_path}/{message_type}/standard.json
93/// 2. {base_path}/{message_type}/default.json
94/// 3. First .json file in {base_path}/{message_type}/
95pub fn find_scenario_for_message_type_with_config(
96    message_type: &str,
97    config: &ScenarioConfig,
98) -> Result<Value> {
99    for base_path in &config.base_paths {
100        let mt_dir = base_path.join(message_type.to_lowercase());
101
102        // Check if directory exists
103        if !mt_dir.exists() {
104            continue;
105        }
106
107        // Try standard.json first
108        let standard_path = mt_dir.join("standard.json");
109        if standard_path.exists() {
110            return load_scenario_json(standard_path);
111        }
112
113        // Try default.json
114        let default_path = mt_dir.join("default.json");
115        if default_path.exists() {
116            return load_scenario_json(default_path);
117        }
118
119        // Find the first .json file
120        if let Ok(entries) = fs::read_dir(&mt_dir) {
121            for entry in entries.flatten() {
122                let path = entry.path();
123
124                if path.extension().and_then(|s| s.to_str()) == Some("json") {
125                    return load_scenario_json(path);
126                }
127            }
128        }
129    }
130
131    let searched_paths: Vec<String> = config
132        .base_paths
133        .iter()
134        .map(|p| format!("{}/{}", p.display(), message_type.to_lowercase()))
135        .collect();
136
137    Err(ParseError::InvalidFormat {
138        message: format!(
139            "No test scenarios found for message type: {}. Searched in: {}",
140            message_type.to_lowercase(),
141            searched_paths.join(", ")
142        ),
143    })
144}
145
146/// Find and load a scenario for a specific message type (backward compatibility)
147///
148/// Looks for configuration files in the following order:
149/// 1. test_scenarios/{message_type}/standard.json
150/// 2. test_scenarios/{message_type}/default.json
151/// 3. First .json file in test_scenarios/{message_type}/
152pub fn find_scenario_for_message_type(message_type: &str) -> Result<Value> {
153    find_scenario_for_message_type_with_config(message_type, &ScenarioConfig::default())
154}
155
156/// Find and load a specific scenario by name with custom configuration
157pub fn find_scenario_by_name_with_config(
158    message_type: &str,
159    scenario_name: &str,
160    config: &ScenarioConfig,
161) -> Result<Value> {
162    for base_path in &config.base_paths {
163        let scenario_path = base_path
164            .join(message_type.to_lowercase())
165            .join(format!("{scenario_name}.json"));
166
167        if scenario_path.exists() {
168            return load_scenario_json(scenario_path);
169        }
170    }
171
172    let tried_paths: Vec<String> = config
173        .base_paths
174        .iter()
175        .map(|p| {
176            format!(
177                "{}/{}/{}.json",
178                p.display(),
179                message_type.to_lowercase(),
180                scenario_name
181            )
182        })
183        .collect();
184
185    Err(ParseError::InvalidFormat {
186        message: format!(
187            "Scenario '{}' not found for {}. Tried paths: {}",
188            scenario_name,
189            message_type,
190            tried_paths.join(", ")
191        ),
192    })
193}
194
195/// Find and load a specific scenario by name (backward compatibility)
196pub fn find_scenario_by_name(message_type: &str, scenario_name: &str) -> Result<Value> {
197    find_scenario_by_name_with_config(message_type, scenario_name, &ScenarioConfig::default())
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::fs;
204    use tempfile::TempDir;
205
206    #[test]
207    fn test_scenario_config_default() {
208        let config = ScenarioConfig::default();
209        assert_eq!(config.base_paths.len(), 2);
210        assert_eq!(config.base_paths[0], PathBuf::from("test_scenarios"));
211        assert_eq!(config.base_paths[1], PathBuf::from("../test_scenarios"));
212    }
213
214    #[test]
215    fn test_scenario_config_with_paths() {
216        let paths = vec![
217            PathBuf::from("/custom/path1"),
218            PathBuf::from("/custom/path2"),
219        ];
220        let config = ScenarioConfig::with_paths(paths.clone());
221        assert_eq!(config.base_paths, paths);
222    }
223
224    #[test]
225    fn test_scenario_config_add_path() {
226        let config = ScenarioConfig::new()
227            .add_path(PathBuf::from("/path1"))
228            .add_path(PathBuf::from("/path2"));
229        assert!(config.base_paths.contains(&PathBuf::from("/path1")));
230        assert!(config.base_paths.contains(&PathBuf::from("/path2")));
231    }
232
233    #[test]
234    fn test_scenario_config_set_paths() {
235        let config = ScenarioConfig::new()
236            .add_path(PathBuf::from("/old"))
237            .set_paths(vec![PathBuf::from("/new1"), PathBuf::from("/new2")]);
238        assert_eq!(config.base_paths.len(), 2);
239        assert!(!config.base_paths.contains(&PathBuf::from("/old")));
240        assert!(config.base_paths.contains(&PathBuf::from("/new1")));
241        assert!(config.base_paths.contains(&PathBuf::from("/new2")));
242    }
243
244    #[test]
245    fn test_parse_env_paths_unix() {
246        #[cfg(not(windows))]
247        {
248            let paths = parse_env_paths("/path1:/path2:/path3");
249            assert_eq!(paths.len(), 3);
250            assert_eq!(paths[0], PathBuf::from("/path1"));
251            assert_eq!(paths[1], PathBuf::from("/path2"));
252            assert_eq!(paths[2], PathBuf::from("/path3"));
253        }
254    }
255
256    #[test]
257    fn test_parse_env_paths_empty_segments() {
258        #[cfg(not(windows))]
259        {
260            let paths = parse_env_paths("/path1::/path2:");
261            assert_eq!(paths.len(), 2);
262            assert_eq!(paths[0], PathBuf::from("/path1"));
263            assert_eq!(paths[1], PathBuf::from("/path2"));
264        }
265    }
266
267    #[test]
268    fn test_find_scenario_with_custom_config() {
269        // Create a temporary directory for test scenarios
270        let temp_dir = TempDir::new().unwrap();
271        let mt103_dir = temp_dir.path().join("mt103");
272        fs::create_dir(&mt103_dir).unwrap();
273
274        // Create a test scenario file
275        let scenario_json = r#"{
276            "basic_header": {
277                "app_id": "F",
278                "service_id": "01",
279                "ltp": "BANKUS33XXXX",
280                "session_number": "0001",
281                "sequence_number": "000001"
282            }
283        }"#;
284
285        let scenario_path = mt103_dir.join("test_scenario.json");
286        fs::write(&scenario_path, scenario_json).unwrap();
287
288        // Test with custom config
289        let config = ScenarioConfig::with_paths(vec![temp_dir.path().to_path_buf()]);
290
291        // This should find our test scenario
292        let result = find_scenario_for_message_type_with_config("MT103", &config);
293        assert!(result.is_ok());
294
295        // Test finding by name
296        let result = find_scenario_by_name_with_config("MT103", "test_scenario", &config);
297        assert!(result.is_ok());
298    }
299
300    #[test]
301    fn test_scenario_not_found_error() {
302        let config = ScenarioConfig::with_paths(vec![PathBuf::from("/nonexistent/path")]);
303
304        let result = find_scenario_for_message_type_with_config("MT999", &config);
305        assert!(result.is_err());
306
307        if let Err(e) = result {
308            let error_msg = format!("{:?}", e);
309            assert!(error_msg.contains("No test scenarios found"));
310            assert!(error_msg.contains("mt999"));
311        }
312    }
313}