mx_message/
scenario_config.rs1use crate::error::ValidationError;
21use serde_json::Value;
22use std::env;
23use std::fs;
24use std::path::{Path, PathBuf};
25
26type Result<T> = std::result::Result<T, ValidationError>;
27
28#[derive(Debug, Clone)]
30pub struct ScenarioConfig {
31 pub base_paths: Vec<PathBuf>,
33}
34
35impl Default for ScenarioConfig {
36 fn default() -> Self {
37 if let Ok(env_paths) = env::var("MX_SCENARIO_PATH") {
39 let paths = parse_env_paths(&env_paths);
40 if !paths.is_empty() {
41 return Self { base_paths: paths };
42 }
43 }
44
45 Self {
47 base_paths: vec![
48 PathBuf::from("test_scenarios"),
49 PathBuf::from("../test_scenarios"),
50 ],
51 }
52 }
53}
54
55impl ScenarioConfig {
56 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn with_paths(paths: Vec<PathBuf>) -> Self {
63 Self { base_paths: paths }
64 }
65
66 pub fn add_path(mut self, path: PathBuf) -> Self {
68 self.base_paths.push(path);
69 self
70 }
71
72 pub fn set_paths(mut self, paths: Vec<PathBuf>) -> Self {
74 self.base_paths = paths;
75 self
76 }
77}
78
79fn parse_env_paths(env_value: &str) -> Vec<PathBuf> {
81 #[cfg(windows)]
83 let separator = ';';
84 #[cfg(not(windows))]
85 let separator = ':';
86
87 env_value
88 .split(separator)
89 .filter(|s| !s.is_empty())
90 .map(PathBuf::from)
91 .collect()
92}
93
94pub fn load_scenario_json<P: AsRef<Path>>(path: P) -> Result<Value> {
96 let content = fs::read_to_string(path)
97 .map_err(|e| ValidationError::new(9998, format!("Failed to read scenario file: {e}")))?;
98
99 serde_json::from_str(&content)
100 .map_err(|e| ValidationError::new(9998, format!("Failed to parse scenario JSON: {e}")))
101}
102
103pub fn find_scenario_for_message_type_with_config(
110 message_type: &str,
111 config: &ScenarioConfig,
112) -> Result<Value> {
113 for base_path in &config.base_paths {
114 let mt_dir = base_path.join(message_type.to_lowercase());
115
116 if !mt_dir.exists() {
118 continue;
119 }
120
121 let standard_path = mt_dir.join("standard.json");
123 if standard_path.exists() {
124 return load_scenario_json(standard_path);
125 }
126
127 let default_path = mt_dir.join("default.json");
129 if default_path.exists() {
130 return load_scenario_json(default_path);
131 }
132
133 if let Ok(entries) = fs::read_dir(&mt_dir) {
135 for entry in entries.flatten() {
136 let path = entry.path();
137
138 if path.extension().and_then(|s| s.to_str()) == Some("json") {
139 return load_scenario_json(path);
140 }
141 }
142 }
143 }
144
145 let searched_paths: Vec<String> = config
146 .base_paths
147 .iter()
148 .map(|p| format!("{}/{}", p.display(), message_type.to_lowercase()))
149 .collect();
150
151 Err(ValidationError::new(
152 9998,
153 format!(
154 "No test scenarios found for message type: {}. Searched in: {}",
155 message_type.to_lowercase(),
156 searched_paths.join(", ")
157 ),
158 ))
159}
160
161pub fn find_scenario_for_message_type(message_type: &str) -> Result<Value> {
168 find_scenario_for_message_type_with_config(message_type, &ScenarioConfig::default())
169}
170
171pub fn find_scenario_by_name_with_config(
173 message_type: &str,
174 scenario_name: &str,
175 config: &ScenarioConfig,
176) -> Result<Value> {
177 for base_path in &config.base_paths {
178 let scenario_path = base_path
179 .join(message_type.to_lowercase())
180 .join(format!("{scenario_name}.json"));
181
182 if scenario_path.exists() {
183 return load_scenario_json(scenario_path);
184 }
185 }
186
187 let tried_paths: Vec<String> = config
188 .base_paths
189 .iter()
190 .map(|p| {
191 format!(
192 "{}/{}/{}.json",
193 p.display(),
194 message_type.to_lowercase(),
195 scenario_name
196 )
197 })
198 .collect();
199
200 Err(ValidationError::new(
201 9998,
202 format!(
203 "Scenario '{}' not found for {}. Tried paths: {}",
204 scenario_name,
205 message_type,
206 tried_paths.join(", ")
207 ),
208 ))
209}
210
211pub fn find_scenario_by_name(message_type: &str, scenario_name: &str) -> Result<Value> {
213 find_scenario_by_name_with_config(message_type, scenario_name, &ScenarioConfig::default())
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use std::fs;
220 use tempfile::TempDir;
221
222 #[test]
223 fn test_scenario_config_default() {
224 let config = ScenarioConfig::default();
225 assert_eq!(config.base_paths.len(), 2);
226 assert_eq!(config.base_paths[0], PathBuf::from("test_scenarios"));
227 assert_eq!(config.base_paths[1], PathBuf::from("../test_scenarios"));
228 }
229
230 #[test]
231 fn test_scenario_config_with_paths() {
232 let paths = vec![
233 PathBuf::from("/custom/path1"),
234 PathBuf::from("/custom/path2"),
235 ];
236 let config = ScenarioConfig::with_paths(paths.clone());
237 assert_eq!(config.base_paths, paths);
238 }
239
240 #[test]
241 fn test_scenario_config_add_path() {
242 let config = ScenarioConfig::new()
243 .add_path(PathBuf::from("/path1"))
244 .add_path(PathBuf::from("/path2"));
245 assert!(config.base_paths.contains(&PathBuf::from("/path1")));
246 assert!(config.base_paths.contains(&PathBuf::from("/path2")));
247 }
248
249 #[test]
250 fn test_scenario_config_set_paths() {
251 let config = ScenarioConfig::new()
252 .add_path(PathBuf::from("/old"))
253 .set_paths(vec![PathBuf::from("/new1"), PathBuf::from("/new2")]);
254 assert_eq!(config.base_paths.len(), 2);
255 assert!(!config.base_paths.contains(&PathBuf::from("/old")));
256 assert!(config.base_paths.contains(&PathBuf::from("/new1")));
257 assert!(config.base_paths.contains(&PathBuf::from("/new2")));
258 }
259
260 #[test]
261 fn test_parse_env_paths_unix() {
262 #[cfg(not(windows))]
263 {
264 let paths = parse_env_paths("/path1:/path2:/path3");
265 assert_eq!(paths.len(), 3);
266 assert_eq!(paths[0], PathBuf::from("/path1"));
267 assert_eq!(paths[1], PathBuf::from("/path2"));
268 assert_eq!(paths[2], PathBuf::from("/path3"));
269 }
270 }
271
272 #[test]
273 fn test_parse_env_paths_empty_segments() {
274 #[cfg(not(windows))]
275 {
276 let paths = parse_env_paths("/path1::/path2:");
277 assert_eq!(paths.len(), 2);
278 assert_eq!(paths[0], PathBuf::from("/path1"));
279 assert_eq!(paths[1], PathBuf::from("/path2"));
280 }
281 }
282
283 #[test]
284 fn test_find_scenario_with_custom_config() {
285 let temp_dir = TempDir::new().unwrap();
287 let pacs008_dir = temp_dir.path().join("pacs008");
288 fs::create_dir(&pacs008_dir).unwrap();
289
290 let scenario_json = r#"{
292 "variables": {
293 "msg_id": {"fake": ["alphanumeric", 16]}
294 },
295 "schema": {
296 "GrpHdr": {
297 "MsgId": {"var": "msg_id"}
298 }
299 }
300 }"#;
301
302 let scenario_path = pacs008_dir.join("test_scenario.json");
303 fs::write(&scenario_path, scenario_json).unwrap();
304
305 let config = ScenarioConfig::with_paths(vec![temp_dir.path().to_path_buf()]);
307
308 let result = find_scenario_for_message_type_with_config("pacs008", &config);
310 assert!(result.is_ok());
311
312 let result = find_scenario_by_name_with_config("pacs008", "test_scenario", &config);
314 assert!(result.is_ok());
315 }
316
317 #[test]
318 fn test_scenario_not_found_error() {
319 let config = ScenarioConfig::with_paths(vec![PathBuf::from("/nonexistent/path")]);
320
321 let result = find_scenario_for_message_type_with_config("pacs999", &config);
322 assert!(result.is_err());
323
324 if let Err(e) = result {
325 let error_msg = format!("{e:?}");
326 assert!(error_msg.contains("No test scenarios found"));
327 assert!(error_msg.contains("pacs999"));
328 }
329 }
330}