dynamic_cli/config/
loader.rs1use crate::config::schema::CommandsConfig;
22use crate::error::{ConfigError, DynamicCliError, Result};
23use std::fs;
24use std::path::Path;
25
26pub fn load_config<P: AsRef<Path>>(path: P) -> Result<CommandsConfig> {
60 let path = path.as_ref();
61
62 if !path.exists() {
64 return Err(ConfigError::FileNotFound {
65 path: path.to_path_buf(),
66 }
67 .into());
68 }
69
70 let extension = path
72 .extension()
73 .and_then(|ext| ext.to_str())
74 .ok_or_else(|| ConfigError::UnsupportedFormat {
75 extension: "<none>".to_string(),
76 })?;
77
78 let content = fs::read_to_string(path).map_err(DynamicCliError::from)?;
80
81 match extension.to_lowercase().as_str() {
83 "yaml" | "yml" => load_yaml(&content),
84 "json" => load_json(&content),
85 other => Err(ConfigError::UnsupportedFormat {
86 extension: other.to_string(),
87 }
88 .into()),
89 }
90}
91
92pub fn load_yaml(content: &str) -> Result<CommandsConfig> {
127 serde_yaml::from_str(content).map_err(|e| {
128 ConfigError::yaml_parse_with_location(e).into()
130 })
131}
132
133pub fn load_json(content: &str) -> Result<CommandsConfig> {
171 serde_json::from_str(content).map_err(|e| {
172 ConfigError::json_parse_with_location(e).into()
174 })
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::io::Write;
181 use tempfile::NamedTempFile;
182
183 fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
185 let mut file = tempfile::Builder::new()
186 .suffix(extension)
187 .tempfile()
188 .unwrap();
189
190 file.write_all(content.as_bytes()).unwrap();
191 file.flush().unwrap();
192 file
193 }
194
195 #[test]
196 fn test_load_yaml_valid() {
197 let yaml = r#"
198metadata:
199 version: "1.0.0"
200 prompt: "test"
201 prompt_suffix: " > "
202commands:
203 - name: hello
204 aliases: []
205 description: "Say hello"
206 required: false
207 arguments: []
208 options: []
209 implementation: "hello_handler"
210global_options: []
211 "#;
212
213 let config = load_yaml(yaml).unwrap();
214
215 assert_eq!(config.metadata.version, "1.0.0");
216 assert_eq!(config.metadata.prompt, "test");
217 assert_eq!(config.commands.len(), 1);
218 assert_eq!(config.commands[0].name, "hello");
219 }
220
221 #[test]
222 fn test_load_yaml_invalid_syntax() {
223 let yaml = r#"
224metadata:
225 version: "1.0.0"
226 prompt: "test"
227commands: [
228 "#; let result = load_yaml(yaml);
231
232 assert!(result.is_err());
233 match result.unwrap_err() {
234 DynamicCliError::Config(ConfigError::YamlParse { .. }) => {}
235 other => panic!("Expected YamlParse error, got {:?}", other),
236 }
237 }
238
239 #[test]
240 fn test_load_json_valid() {
241 let json = r#"
242{
243 "metadata": {
244 "version": "1.0.0",
245 "prompt": "test",
246 "prompt_suffix": " > "
247 },
248 "commands": [
249 {
250 "name": "hello",
251 "aliases": [],
252 "description": "Say hello",
253 "required": false,
254 "arguments": [],
255 "options": [],
256 "implementation": "hello_handler"
257 }
258 ],
259 "global_options": []
260}
261 "#;
262
263 let config = load_json(json).unwrap();
264
265 assert_eq!(config.metadata.version, "1.0.0");
266 assert_eq!(config.commands.len(), 1);
267 assert_eq!(config.commands[0].name, "hello");
268 }
269
270 #[test]
271 fn test_load_json_invalid_syntax() {
272 let json = r#"
273{
274 "metadata": {
275 "version": "1.0.0",
276 "prompt": "test"
277 },
278 "commands": [
279 "#; let result = load_json(json);
282
283 assert!(result.is_err());
284 match result.unwrap_err() {
285 DynamicCliError::Config(ConfigError::JsonParse { .. }) => {}
286 other => panic!("Expected JsonParse error, got {:?}", other),
287 }
288 }
289
290 #[test]
291 fn test_load_config_yaml_file() {
292 let yaml = r#"
293metadata:
294 version: "1.0.0"
295 prompt: "test"
296commands: []
297global_options: []
298 "#;
299
300 let file = create_temp_file(yaml, ".yaml");
301 let config = load_config(file.path()).unwrap();
302
303 assert_eq!(config.metadata.version, "1.0.0");
304 }
305
306 #[test]
307 fn test_load_config_yml_extension() {
308 let yaml = r#"
309metadata:
310 version: "1.0.0"
311 prompt: "test"
312commands: []
313global_options: []
314 "#;
315
316 let file = create_temp_file(yaml, ".yml");
317 let config = load_config(file.path()).unwrap();
318
319 assert_eq!(config.metadata.version, "1.0.0");
320 }
321
322 #[test]
323 fn test_load_config_json_file() {
324 let json = r#"
325{
326 "metadata": {
327 "version": "1.0.0",
328 "prompt": "test"
329 },
330 "commands": [],
331 "global_options": []
332}
333 "#;
334
335 let file = create_temp_file(json, ".json");
336 let config = load_config(file.path()).unwrap();
337
338 assert_eq!(config.metadata.version, "1.0.0");
339 }
340
341 #[test]
342 fn test_load_config_file_not_found() {
343 let result = load_config("nonexistent_file.yaml");
344
345 assert!(result.is_err());
346 match result.unwrap_err() {
347 DynamicCliError::Config(ConfigError::FileNotFound { path }) => {
348 assert!(path.to_str().unwrap().contains("nonexistent_file.yaml"));
349 }
350 other => panic!("Expected FileNotFound error, got {:?}", other),
351 }
352 }
353
354 #[test]
355 fn test_load_config_unsupported_extension() {
356 let content = "some content";
357 let file = create_temp_file(content, ".txt");
358
359 let result = load_config(file.path());
360
361 assert!(result.is_err());
362 match result.unwrap_err() {
363 DynamicCliError::Config(ConfigError::UnsupportedFormat { extension }) => {
364 assert_eq!(extension, "txt");
365 }
366 other => panic!("Expected UnsupportedFormat error, got {:?}", other),
367 }
368 }
369
370 #[test]
371 fn test_load_config_no_extension() {
372 let content = "some content";
373
374 let mut file = tempfile::Builder::new()
376 .suffix("") .tempfile()
378 .unwrap();
379
380 file.write_all(content.as_bytes()).unwrap();
381 file.flush().unwrap();
382
383 let path_without_ext = file.path().with_file_name("configfile");
385 std::fs::copy(file.path(), &path_without_ext).unwrap();
386
387 let result = load_config(&path_without_ext);
388
389 let _ = std::fs::remove_file(&path_without_ext);
391
392 assert!(result.is_err());
393 match result.unwrap_err() {
394 DynamicCliError::Config(ConfigError::UnsupportedFormat { .. }) => {}
395 other => panic!("Expected UnsupportedFormat error, got {:?}", other),
396 }
397 }
398
399 #[test]
400 fn test_load_yaml_with_complex_structure() {
401 let yaml = r#"
402metadata:
403 version: "2.0.0"
404 prompt: "myapp"
405 prompt_suffix: " $ "
406commands:
407 - name: process
408 aliases: [proc, p]
409 description: "Process data"
410 required: true
411 arguments:
412 - name: input
413 arg_type: path
414 required: true
415 description: "Input file"
416 validation:
417 - must_exist: true
418 - extensions: [csv, tsv]
419 options:
420 - name: output
421 short: o
422 long: output
423 option_type: path
424 required: false
425 default: "output.txt"
426 description: "Output file"
427 choices: []
428 implementation: "process_handler"
429global_options:
430 - name: verbose
431 short: v
432 long: verbose
433 option_type: bool
434 required: false
435 description: "Verbose output"
436 choices: []
437 "#;
438
439 let config = load_yaml(yaml).unwrap();
440
441 assert_eq!(config.metadata.version, "2.0.0");
442 assert_eq!(config.commands.len(), 1);
443 assert_eq!(config.commands[0].arguments.len(), 1);
444 assert_eq!(config.commands[0].options.len(), 1);
445 assert_eq!(config.global_options.len(), 1);
446 }
447
448 #[test]
449 fn test_load_json_with_complex_structure() {
450 let json = r#"
451{
452 "metadata": {
453 "version": "2.0.0",
454 "prompt": "myapp"
455 },
456 "commands": [
457 {
458 "name": "process",
459 "aliases": ["proc"],
460 "description": "Process data",
461 "required": true,
462 "arguments": [
463 {
464 "name": "input",
465 "arg_type": "path",
466 "required": true,
467 "description": "Input file",
468 "validation": [
469 {"must_exist": true},
470 {"extensions": ["csv"]}
471 ]
472 }
473 ],
474 "options": [],
475 "implementation": "process_handler"
476 }
477 ],
478 "global_options": []
479}
480 "#;
481
482 let config = load_json(json).unwrap();
483
484 assert_eq!(config.metadata.version, "2.0.0");
485 assert_eq!(config.commands[0].arguments.len(), 1);
486 }
487
488 #[test]
489 fn test_error_contains_position_yaml() {
490 let yaml_syntax_error = "{{{";
492
493 let result = load_yaml(yaml_syntax_error);
494
495 assert!(result.is_err());
497
498 match result.unwrap_err() {
500 DynamicCliError::Config(ConfigError::YamlParse { .. }) => {
501 }
503 other => panic!("Expected YamlParse error, got {:?}", other),
504 }
505 }
506
507 #[test]
508 fn test_case_insensitive_extension() {
509 let yaml = r#"
510metadata:
511 version: "1.0.0"
512 prompt: "test"
513commands: []
514global_options: []
515 "#;
516
517 let file = create_temp_file(yaml, ".YAML");
519 let config = load_config(file.path()).unwrap();
520
521 assert_eq!(config.metadata.version, "1.0.0");
522 }
523}