1use colored::Colorize;
28
29#[cfg(feature = "wasm-plugins")]
30use crate::error::WasmError;
31use crate::error::{
32 ConfigError, DynamicCliError, ExecutionError, ParseError, RegistryError, ValidationError,
33};
34
35fn color_error(s: &str) -> String {
41 s.red().bold().to_string()
42}
43
44fn color_question(s: &str) -> String {
46 s.yellow().bold().to_string()
47}
48
49fn color_bullet(s: &str) -> String {
51 s.cyan().to_string()
52}
53
54fn color_suggestion(s: &str) -> String {
56 s.green().to_string()
57}
58
59fn color_info(s: &str) -> String {
61 s.blue().bold().to_string()
62}
63
64fn color_type_name(s: &str) -> String {
66 s.cyan().to_string()
67}
68
69fn color_arg_name(s: &str) -> String {
71 s.yellow().to_string()
72}
73
74fn color_value(s: &str) -> String {
76 s.red().to_string()
77}
78
79fn color_dimmed(s: &str) -> String {
81 s.dimmed().to_string()
82}
83
84pub fn display_error(error: &DynamicCliError) {
104 eprintln!("{}", format_error(error));
105}
106
107pub fn format_error(error: &DynamicCliError) -> String {
140 let mut output = String::new();
141
142 output.push_str(&format!("{} ", color_error("Error:")));
143
144 match error {
145 DynamicCliError::Parse(e) => format_parse_error(&mut output, e),
146 DynamicCliError::Config(e) => format_config_error(&mut output, e),
147 DynamicCliError::Validation(e) => format_validation_error(&mut output, e),
148 DynamicCliError::Execution(e) => format_execution_error(&mut output, e),
149 DynamicCliError::Registry(e) => format_registry_error(&mut output, e),
150 #[cfg(feature = "wasm-plugins")]
151 DynamicCliError::Wasm(e) => format_wasm_error(&mut output, e),
152 DynamicCliError::Io(e) => output.push_str(&format!("{}\n", e)),
153 }
154
155 output
156}
157
158fn format_parse_error(output: &mut String, error: &ParseError) {
164 output.push_str(&format!("{}\n", error));
165
166 match error {
167 ParseError::UnknownCommand { suggestions, .. } if !suggestions.is_empty() => {
168 output.push_str(&format!("\n{} Did you mean:\n", color_question("?")));
169 for s in suggestions {
170 output.push_str(&format!(
171 " {} {}\n",
172 color_bullet("•"),
173 color_suggestion(s)
174 ));
175 }
176 }
177
178 ParseError::UnknownOption { suggestions, .. } if !suggestions.is_empty() => {
179 output.push_str(&format!("\n{} Did you mean:\n", color_question("?")));
180 for s in suggestions {
181 output.push_str(&format!(
182 " {} {}\n",
183 color_bullet("•"),
184 color_suggestion(s)
185 ));
186 }
187 }
188
189 ParseError::TypeParseError {
190 arg_name,
191 expected_type,
192 value,
193 ..
194 } => {
195 output.push_str(&format!(
196 "\n{} Expected type {} for argument {}, got: {}\n",
197 color_info("ℹ"),
198 color_type_name(expected_type),
199 color_arg_name(arg_name),
200 color_value(value)
201 ));
202 }
203
204 ParseError::MissingArgument { suggestion, .. }
205 | ParseError::MissingOption { suggestion, .. }
206 | ParseError::TooManyArguments { suggestion, .. }
207 | ParseError::UnknownOptionParameter { suggestion, .. }
208 | ParseError::MissingRequiredOptionParameter { suggestion, .. }
209 | ParseError::UnknownDiscriminant { suggestion, .. }
210 | ParseError::DuplicateOptionOccurrence { suggestion, .. } => {
211 append_suggestion(output, suggestion.as_deref());
212 }
213
214 _ => {}
215 }
216}
217
218fn format_config_error(output: &mut String, error: &ConfigError) {
220 match error {
221 ConfigError::YamlParse {
222 source,
223 line,
224 column,
225 } => {
226 output.push_str(&format!("{}\n", source));
227 if let (Some(l), Some(c)) = (line, column) {
228 output.push_str(&format!(
229 " {} line {}, column {}\n",
230 color_dimmed("at"),
231 color_arg_name(&l.to_string()),
232 color_arg_name(&c.to_string())
233 ));
234 }
235 }
236
237 ConfigError::JsonParse {
238 source,
239 line,
240 column,
241 } => {
242 output.push_str(&format!("{}\n", source));
243 output.push_str(&format!(
244 " {} line {}, column {}\n",
245 color_dimmed("at"),
246 color_arg_name(&line.to_string()),
247 color_arg_name(&column.to_string())
248 ));
249 }
250
251 ConfigError::InvalidSchema {
252 reason,
253 path,
254 suggestion,
255 } => {
256 output.push_str(&format!("{}\n", reason));
257 if let Some(p) = path {
258 output.push_str(&format!(
259 " {} {}\n",
260 color_dimmed("in"),
261 color_type_name(p)
262 ));
263 }
264 append_suggestion(output, suggestion.as_deref());
265 }
266
267 ConfigError::FileNotFound { suggestion, .. }
268 | ConfigError::UnsupportedFormat { suggestion, .. }
269 | ConfigError::DuplicateCommand { suggestion, .. }
270 | ConfigError::UnknownType { suggestion, .. }
271 | ConfigError::Inconsistency { suggestion, .. } => {
272 output.push_str(&format!("{}\n", error));
273 append_suggestion(output, suggestion.as_deref());
274 }
275 }
276}
277
278fn format_validation_error(output: &mut String, error: &ValidationError) {
280 output.push_str(&format!("{}\n", error));
281
282 let suggestion = match error {
283 ValidationError::FileNotFound { suggestion, .. } => suggestion.as_deref(),
284 ValidationError::OutOfRange { suggestion, .. } => suggestion.as_deref(),
285 ValidationError::CustomConstraint { suggestion, .. } => suggestion.as_deref(),
286 ValidationError::MissingDependency { suggestion, .. } => suggestion.as_deref(),
287 ValidationError::MutuallyExclusive { suggestion, .. } => suggestion.as_deref(),
288 ValidationError::InvalidExtension { .. } => None,
290 };
291
292 append_suggestion(output, suggestion);
293}
294
295fn format_execution_error(output: &mut String, error: &ExecutionError) {
297 output.push_str(&format!("{}\n", error));
298
299 let suggestion = match error {
300 ExecutionError::HandlerNotFound { suggestion, .. } => suggestion.as_deref(),
301 ExecutionError::ContextDowncastFailed { suggestion, .. } => suggestion.as_deref(),
302 ExecutionError::InvalidContextState { suggestion, .. } => suggestion.as_deref(),
303 ExecutionError::CommandFailed(_) | ExecutionError::Interrupted => None,
305 };
306
307 append_suggestion(output, suggestion);
308}
309
310fn format_registry_error(output: &mut String, error: &RegistryError) {
312 output.push_str(&format!("{}\n", error));
313
314 let suggestion = match error {
315 RegistryError::DuplicateRegistration { suggestion, .. } => suggestion.as_deref(),
316 RegistryError::DuplicateAlias { suggestion, .. } => suggestion.as_deref(),
317 RegistryError::MissingHandler { suggestion, .. } => suggestion.as_deref(),
318 };
319
320 append_suggestion(output, suggestion);
321}
322
323#[cfg(feature = "wasm-plugins")]
330fn format_wasm_error(output: &mut String, error: &WasmError) {
331 output.push_str(&format!("{}\n", error));
332
333 let suggestion = match error {
334 WasmError::LoadFailed { suggestion, .. } => suggestion.as_deref(),
335 WasmError::FunctionNotFound { suggestion, .. } => suggestion.as_deref(),
336 WasmError::GuestError { .. } => None,
337 WasmError::SerializationFailed(_) => None,
338 WasmError::MemoryAccessFailed { .. } => None,
339 };
340
341 append_suggestion(output, suggestion);
342}
343
344fn append_suggestion(output: &mut String, suggestion: Option<&str>) {
358 if let Some(s) = suggestion {
359 output.push_str(&format!(" {} {}\n", color_info("ℹ"), s));
360 }
361}
362
363#[cfg(test)]
368mod tests {
369 use super::*;
370 use std::path::PathBuf;
371
372 #[test]
375 fn test_format_config_file_not_found_contains_path() {
376 let error: DynamicCliError = ConfigError::FileNotFound {
377 path: PathBuf::from("test.yaml"),
378 suggestion: None,
379 }
380 .into();
381
382 let formatted = format_error(&error);
383 assert!(formatted.contains("Error:"));
384 assert!(formatted.contains("test.yaml"));
385 }
386
387 #[test]
388 fn test_format_config_file_not_found_with_suggestion() {
389 let error: DynamicCliError = ConfigError::FileNotFound {
390 path: PathBuf::from("test.yaml"),
391 suggestion: Some("Verify the path.".to_string()),
392 }
393 .into();
394
395 let formatted = format_error(&error);
396 assert!(formatted.contains("Verify the path."));
397 }
398
399 #[test]
400 fn test_format_config_file_not_found_no_suggestion_no_hint_line() {
401 let error: DynamicCliError = ConfigError::FileNotFound {
402 path: PathBuf::from("test.yaml"),
403 suggestion: None,
404 }
405 .into();
406
407 let formatted = format_error(&error);
408 assert!(!formatted.contains('ℹ'));
410 }
411
412 #[test]
413 fn test_format_config_unsupported_format_with_suggestion() {
414 let error: DynamicCliError = ConfigError::UnsupportedFormat {
415 extension: ".toml".to_string(),
416 suggestion: Some("Use .yaml instead.".to_string()),
417 }
418 .into();
419
420 let formatted = format_error(&error);
421 assert!(formatted.contains(".toml"));
422 assert!(formatted.contains("Use .yaml instead."));
423 }
424
425 #[test]
426 fn test_format_config_yaml_parse_contains_location() {
427 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: [")
428 .err()
429 .unwrap();
430
431 let error: DynamicCliError = ConfigError::yaml_parse_with_location(yaml_error).into();
432 let formatted = format_error(&error);
433 assert!(formatted.contains("Error:"));
434 }
435
436 #[test]
437 fn test_format_config_invalid_schema_with_path_and_suggestion() {
438 let error: DynamicCliError = ConfigError::InvalidSchema {
439 reason: "missing field".to_string(),
440 path: Some("commands[0]".to_string()),
441 suggestion: Some("Add a name field.".to_string()),
442 }
443 .into();
444
445 let formatted = format_error(&error);
446 assert!(formatted.contains("missing field"));
447 assert!(formatted.contains("commands[0]"));
448 assert!(formatted.contains("Add a name field."));
449 }
450
451 #[test]
454 fn test_format_parse_unknown_command_with_suggestions() {
455 let error: DynamicCliError = ParseError::UnknownCommand {
456 command: "simulat".to_string(),
457 suggestions: vec!["simulate".to_string(), "validation".to_string()],
458 }
459 .into();
460
461 let formatted = format_error(&error);
462 assert!(formatted.contains("Unknown command"));
463 assert!(formatted.contains("simulat"));
464 assert!(formatted.contains("Did you mean"));
465 assert!(formatted.contains("simulate"));
466 }
467
468 #[test]
469 fn test_format_parse_unknown_command_no_suggestions() {
470 let error: DynamicCliError = ParseError::UnknownCommand {
471 command: "xyz".to_string(),
472 suggestions: vec![],
473 }
474 .into();
475
476 let formatted = format_error(&error);
477 assert!(formatted.contains("xyz"));
478 assert!(!formatted.contains("Did you mean"));
479 }
480
481 #[test]
482 fn test_format_parse_missing_argument_with_suggestion() {
483 let error: DynamicCliError = ParseError::MissingArgument {
484 argument: "file".to_string(),
485 command: "process".to_string(),
486 suggestion: Some("Run --help process to see required arguments.".to_string()),
487 }
488 .into();
489
490 let formatted = format_error(&error);
491 assert!(formatted.contains("file"));
492 assert!(formatted.contains("Run --help process"));
493 }
494
495 #[test]
496 fn test_format_parse_missing_option_with_suggestion() {
497 let error: DynamicCliError = ParseError::MissingOption {
498 option: "output".to_string(),
499 command: "export".to_string(),
500 suggestion: Some("Run --help export to see required options.".to_string()),
501 }
502 .into();
503
504 let formatted = format_error(&error);
505 assert!(formatted.contains("output"));
506 assert!(formatted.contains("Run --help export"));
507 }
508
509 #[test]
510 fn test_format_parse_too_many_arguments_with_suggestion() {
511 let error: DynamicCliError = ParseError::TooManyArguments {
512 command: "run".to_string(),
513 expected: 1,
514 got: 3,
515 suggestion: Some("Run --help run for the expected usage.".to_string()),
516 }
517 .into();
518
519 let formatted = format_error(&error);
520 assert!(formatted.contains("run"));
521 assert!(formatted.contains("Run --help run"));
522 }
523
524 #[test]
527 fn test_format_parse_unknown_option_parameter_with_suggestion() {
528 let error: DynamicCliError = ParseError::UnknownOptionParameter {
529 option: "output".to_string(),
530 discriminant: "csv".to_string(),
531 key: "compression".to_string(),
532 valid_keys: vec!["file".to_string(), "resolution".to_string()],
533 suggestion: Some("Run --help export to see valid keys for --output csv.".to_string()),
534 }
535 .into();
536
537 let formatted = format_error(&error);
538 assert!(formatted.contains("compression"));
539 assert!(formatted.contains("csv"));
540 assert!(formatted.contains("file"));
541 assert!(formatted.contains("resolution"));
542 assert!(formatted.contains("Run --help export"));
543 }
544
545 #[test]
546 fn test_format_parse_missing_required_option_parameter_with_suggestion() {
547 let error: DynamicCliError = ParseError::MissingRequiredOptionParameter {
548 option: "output".to_string(),
549 discriminant: "csv".to_string(),
550 key: "file".to_string(),
551 suggestion: Some(
552 "Run --help export to see required keys for --output csv.".to_string(),
553 ),
554 }
555 .into();
556
557 let formatted = format_error(&error);
558 assert!(formatted.contains("file"));
559 assert!(formatted.contains("csv"));
560 assert!(formatted.contains("Run --help export"));
561 }
562
563 #[test]
564 fn test_format_parse_unknown_discriminant_with_suggestion() {
565 let error: DynamicCliError = ParseError::UnknownDiscriminant {
566 option: "output".to_string(),
567 value: "xml".to_string(),
568 valid_choices: vec!["csv".to_string(), "plot".to_string()],
569 suggestion: Some("Run --help export to see valid --output kinds.".to_string()),
570 }
571 .into();
572
573 let formatted = format_error(&error);
574 assert!(formatted.contains("xml"));
575 assert!(formatted.contains("csv"));
576 assert!(formatted.contains("plot"));
577 assert!(formatted.contains("Run --help export"));
578 }
579
580 #[test]
581 fn test_format_parse_duplicate_option_occurrence_with_suggestion() {
582 let error: DynamicCliError = ParseError::DuplicateOptionOccurrence {
583 option: "output".to_string(),
584 discriminant: "csv".to_string(),
585 params: vec![("file".to_string(), "results.csv".to_string())],
586 suggestion: Some(
587 "Remove one of the two identical --output csv occurrences.".to_string(),
588 ),
589 }
590 .into();
591
592 let formatted = format_error(&error);
593 assert!(formatted.contains("results.csv"));
594 assert!(formatted.contains("csv"));
595 assert!(formatted.contains("Remove one of the two identical"));
596 }
597
598 #[test]
599 fn test_debug_new_parse_error_variants_dd024() {
600 let variants: Vec<ParseError> = vec![
603 ParseError::UnknownOptionParameter {
604 option: "output".to_string(),
605 discriminant: "csv".to_string(),
606 key: "compression".to_string(),
607 valid_keys: vec!["file".to_string()],
608 suggestion: None,
609 },
610 ParseError::MissingRequiredOptionParameter {
611 option: "output".to_string(),
612 discriminant: "csv".to_string(),
613 key: "file".to_string(),
614 suggestion: None,
615 },
616 ParseError::UnknownDiscriminant {
617 option: "output".to_string(),
618 value: "xml".to_string(),
619 valid_choices: vec!["csv".to_string()],
620 suggestion: None,
621 },
622 ParseError::DuplicateOptionOccurrence {
623 option: "output".to_string(),
624 discriminant: "csv".to_string(),
625 params: vec![("file".to_string(), "results.csv".to_string())],
626 suggestion: None,
627 },
628 ];
629
630 for variant in variants {
631 let debug_str = format!("{:?}", variant);
632 assert!(!debug_str.is_empty());
633 }
634 }
635
636 #[test]
637 fn test_format_parse_type_parse_error_shows_info_block() {
638 let error: DynamicCliError = ParseError::TypeParseError {
639 arg_name: "count".to_string(),
640 expected_type: "integer".to_string(),
641 value: "abc".to_string(),
642 details: None,
643 }
644 .into();
645
646 let formatted = format_error(&error);
647 assert!(formatted.contains("integer"));
648 assert!(formatted.contains("count"));
649 assert!(formatted.contains("abc"));
650 }
651
652 #[test]
655 fn test_format_validation_file_not_found_with_suggestion() {
656 let error: DynamicCliError = ValidationError::FileNotFound {
657 path: PathBuf::from("data.csv"),
658 arg_name: "input".to_string(),
659 suggestion: Some("Check that the file exists.".to_string()),
660 }
661 .into();
662
663 let formatted = format_error(&error);
664 assert!(formatted.contains("data.csv"));
665 assert!(formatted.contains("Check that the file exists."));
666 }
667
668 #[test]
669 fn test_format_validation_out_of_range_with_suggestion() {
670 let error: DynamicCliError = ValidationError::OutOfRange {
671 arg_name: "percentage".to_string(),
672 value: 150.0,
673 min: 0.0,
674 max: 100.0,
675 suggestion: Some("Value must be between 0 and 100.".to_string()),
676 }
677 .into();
678
679 let formatted = format_error(&error);
680 assert!(formatted.contains("percentage"));
681 assert!(formatted.contains("Value must be between 0 and 100."));
682 }
683
684 #[test]
685 fn test_format_validation_mutually_exclusive_with_suggestion() {
686 let error: DynamicCliError = ValidationError::MutuallyExclusive {
687 arg1: "--verbose".to_string(),
688 arg2: "--quiet".to_string(),
689 suggestion: Some("Remove one of the two conflicting options.".to_string()),
690 }
691 .into();
692
693 let formatted = format_error(&error);
694 assert!(formatted.contains("--verbose"));
695 assert!(formatted.contains("Remove one of the two conflicting options."));
696 }
697
698 #[test]
699 fn test_format_validation_missing_dependency_with_suggestion() {
700 let error: DynamicCliError = ValidationError::MissingDependency {
701 arg_name: "format".to_string(),
702 required_arg: "output".to_string(),
703 suggestion: Some("Add --output to your command.".to_string()),
704 }
705 .into();
706
707 let formatted = format_error(&error);
708 assert!(formatted.contains("format"));
709 assert!(formatted.contains("Add --output to your command."));
710 }
711
712 #[test]
713 fn test_format_validation_invalid_extension_no_suggestion_line() {
714 let error: DynamicCliError = ValidationError::InvalidExtension {
716 arg_name: "input".to_string(),
717 path: PathBuf::from("data.png"),
718 expected: vec![".csv".to_string(), ".tsv".to_string()],
719 }
720 .into();
721
722 let formatted = format_error(&error);
723 assert!(formatted.contains("data.png"));
724 assert!(!formatted.contains('ℹ'));
725 }
726
727 #[test]
730 fn test_format_execution_handler_not_found_with_suggestion() {
731 let error: DynamicCliError = ExecutionError::HandlerNotFound {
732 command: "run".to_string(),
733 implementation: "run_handler".to_string(),
734 suggestion: Some(
735 "Ensure .register_sync_handler(\"run_handler\", ...) was called.".to_string(),
736 ),
737 }
738 .into();
739
740 let formatted = format_error(&error);
741 assert!(formatted.contains("run"));
742 assert!(formatted.contains("run_handler"));
743 assert!(formatted.contains("register_sync_handler"));
744 }
745
746 #[test]
747 fn test_format_execution_context_downcast_failed_with_suggestion() {
748 let error: DynamicCliError = ExecutionError::ContextDowncastFailed {
749 expected_type: "MyCtx".to_string(),
750 suggestion: Some("Check the context type.".to_string()),
751 }
752 .into();
753
754 let formatted = format_error(&error);
755 assert!(formatted.contains("MyCtx"));
756 assert!(formatted.contains("Check the context type."));
757 }
758
759 #[test]
760 fn test_format_execution_interrupted_no_suggestion() {
761 let error: DynamicCliError = ExecutionError::Interrupted.into();
762 let formatted = format_error(&error);
763 assert!(formatted.contains("interrupted"));
764 assert!(!formatted.contains('ℹ'));
765 }
766
767 #[test]
770 fn test_format_registry_missing_handler_with_suggestion() {
771 let error: DynamicCliError = RegistryError::MissingHandler {
772 command: "export".to_string(),
773 suggestion: Some(
774 "Call .register_sync_handler(\"export\", ...) before running.".to_string(),
775 ),
776 }
777 .into();
778
779 let formatted = format_error(&error);
780 assert!(formatted.contains("export"));
781 assert!(formatted.contains("register_sync_handler"));
782 }
783
784 #[test]
785 fn test_format_registry_duplicate_registration_with_suggestion() {
786 let error: DynamicCliError = RegistryError::DuplicateRegistration {
787 name: "run".to_string(),
788 suggestion: Some("Command names must be unique.".to_string()),
789 }
790 .into();
791
792 let formatted = format_error(&error);
793 assert!(formatted.contains("run"));
794 assert!(formatted.contains("Command names must be unique."));
795 }
796
797 #[test]
798 fn test_format_registry_duplicate_alias_with_suggestion() {
799 let error: DynamicCliError = RegistryError::DuplicateAlias {
800 alias: "r".to_string(),
801 existing_command: "run".to_string(),
802 suggestion: Some("Choose a different alias.".to_string()),
803 }
804 .into();
805
806 let formatted = format_error(&error);
807 assert!(formatted.contains("run"));
808 assert!(formatted.contains("Choose a different alias."));
809 }
810
811 #[cfg(feature = "wasm-plugins")]
814 #[test]
815 fn test_format_wasm_function_not_found_with_suggestion() {
816 let error: DynamicCliError = WasmError::FunctionNotFound {
817 function: "dcli_dealloc".to_string(),
818 module: "plugin.wasm".to_string(),
819 suggestion: Some("Export `dcli_dealloc(ptr: i32, size: i32)`.".to_string()),
820 }
821 .into();
822
823 let formatted = format_error(&error);
824 assert!(formatted.contains("dcli_dealloc"));
825 assert!(formatted.contains("Export `dcli_dealloc"));
826 }
827
828 #[cfg(feature = "wasm-plugins")]
829 #[test]
830 fn test_format_wasm_guest_error_without_suggestion_line() {
831 let error: DynamicCliError = WasmError::GuestError {
832 code: 1,
833 message: Some("invalid argument".to_string()),
834 }
835 .into();
836
837 let formatted = format_error(&error);
838 assert!(formatted.contains("invalid argument"));
839 assert!(!formatted.contains('ℹ'));
841 }
842
843 #[test]
846 fn test_display_error_does_not_panic() {
847 let error: DynamicCliError = ConfigError::FileNotFound {
848 path: PathBuf::from("test.yaml"),
849 suggestion: None,
850 }
851 .into();
852 display_error(&error);
854 }
855}