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