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 RegistryError::FaultToleranceMismatch { suggestion, .. } => suggestion.as_deref(),
319 };
320
321 append_suggestion(output, suggestion);
322}
323
324#[cfg(feature = "wasm-plugins")]
331fn format_wasm_error(output: &mut String, error: &WasmError) {
332 output.push_str(&format!("{}\n", error));
333
334 let suggestion = match error {
335 WasmError::LoadFailed { suggestion, .. } => suggestion.as_deref(),
336 WasmError::FunctionNotFound { suggestion, .. } => suggestion.as_deref(),
337 WasmError::GuestError { .. } => None,
338 WasmError::SerializationFailed(_) => None,
339 WasmError::MemoryAccessFailed { .. } => None,
340 };
341
342 append_suggestion(output, suggestion);
343}
344
345fn append_suggestion(output: &mut String, suggestion: Option<&str>) {
359 if let Some(s) = suggestion {
360 output.push_str(&format!(" {} {}\n", color_info("ℹ"), s));
361 }
362}
363
364#[cfg(test)]
369mod tests {
370 use super::*;
371 use std::path::PathBuf;
372
373 #[test]
376 fn test_format_config_file_not_found_contains_path() {
377 let error: DynamicCliError = ConfigError::FileNotFound {
378 path: PathBuf::from("test.yaml"),
379 suggestion: None,
380 }
381 .into();
382
383 let formatted = format_error(&error);
384 assert!(formatted.contains("Error:"));
385 assert!(formatted.contains("test.yaml"));
386 }
387
388 #[test]
389 fn test_format_config_file_not_found_with_suggestion() {
390 let error: DynamicCliError = ConfigError::FileNotFound {
391 path: PathBuf::from("test.yaml"),
392 suggestion: Some("Verify the path.".to_string()),
393 }
394 .into();
395
396 let formatted = format_error(&error);
397 assert!(formatted.contains("Verify the path."));
398 }
399
400 #[test]
401 fn test_format_config_file_not_found_no_suggestion_no_hint_line() {
402 let error: DynamicCliError = ConfigError::FileNotFound {
403 path: PathBuf::from("test.yaml"),
404 suggestion: None,
405 }
406 .into();
407
408 let formatted = format_error(&error);
409 assert!(!formatted.contains('ℹ'));
411 }
412
413 #[test]
414 fn test_format_config_unsupported_format_with_suggestion() {
415 let error: DynamicCliError = ConfigError::UnsupportedFormat {
416 extension: ".toml".to_string(),
417 suggestion: Some("Use .yaml instead.".to_string()),
418 }
419 .into();
420
421 let formatted = format_error(&error);
422 assert!(formatted.contains(".toml"));
423 assert!(formatted.contains("Use .yaml instead."));
424 }
425
426 #[test]
427 fn test_format_config_yaml_parse_contains_location() {
428 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: [")
429 .err()
430 .unwrap();
431
432 let error: DynamicCliError = ConfigError::yaml_parse_with_location(yaml_error).into();
433 let formatted = format_error(&error);
434 assert!(formatted.contains("Error:"));
435 }
436
437 #[test]
438 fn test_format_config_invalid_schema_with_path_and_suggestion() {
439 let error: DynamicCliError = ConfigError::InvalidSchema {
440 reason: "missing field".to_string(),
441 path: Some("commands[0]".to_string()),
442 suggestion: Some("Add a name field.".to_string()),
443 }
444 .into();
445
446 let formatted = format_error(&error);
447 assert!(formatted.contains("missing field"));
448 assert!(formatted.contains("commands[0]"));
449 assert!(formatted.contains("Add a name field."));
450 }
451
452 #[test]
455 fn test_format_parse_unknown_command_with_suggestions() {
456 let error: DynamicCliError = ParseError::UnknownCommand {
457 command: "simulat".to_string(),
458 suggestions: vec!["simulate".to_string(), "validation".to_string()],
459 }
460 .into();
461
462 let formatted = format_error(&error);
463 assert!(formatted.contains("Unknown command"));
464 assert!(formatted.contains("simulat"));
465 assert!(formatted.contains("Did you mean"));
466 assert!(formatted.contains("simulate"));
467 }
468
469 #[test]
470 fn test_format_parse_unknown_command_no_suggestions() {
471 let error: DynamicCliError = ParseError::UnknownCommand {
472 command: "xyz".to_string(),
473 suggestions: vec![],
474 }
475 .into();
476
477 let formatted = format_error(&error);
478 assert!(formatted.contains("xyz"));
479 assert!(!formatted.contains("Did you mean"));
480 }
481
482 #[test]
483 fn test_format_parse_missing_argument_with_suggestion() {
484 let error: DynamicCliError = ParseError::MissingArgument {
485 argument: "file".to_string(),
486 command: "process".to_string(),
487 suggestion: Some("Run --help process to see required arguments.".to_string()),
488 }
489 .into();
490
491 let formatted = format_error(&error);
492 assert!(formatted.contains("file"));
493 assert!(formatted.contains("Run --help process"));
494 }
495
496 #[test]
497 fn test_format_parse_missing_option_with_suggestion() {
498 let error: DynamicCliError = ParseError::MissingOption {
499 option: "output".to_string(),
500 command: "export".to_string(),
501 suggestion: Some("Run --help export to see required options.".to_string()),
502 }
503 .into();
504
505 let formatted = format_error(&error);
506 assert!(formatted.contains("output"));
507 assert!(formatted.contains("Run --help export"));
508 }
509
510 #[test]
511 fn test_format_parse_too_many_arguments_with_suggestion() {
512 let error: DynamicCliError = ParseError::TooManyArguments {
513 command: "run".to_string(),
514 expected: 1,
515 got: 3,
516 suggestion: Some("Run --help run for the expected usage.".to_string()),
517 }
518 .into();
519
520 let formatted = format_error(&error);
521 assert!(formatted.contains("run"));
522 assert!(formatted.contains("Run --help run"));
523 }
524
525 #[test]
528 fn test_format_parse_unknown_option_parameter_with_suggestion() {
529 let error: DynamicCliError = ParseError::UnknownOptionParameter {
530 option: "output".to_string(),
531 discriminant: "csv".to_string(),
532 key: "compression".to_string(),
533 valid_keys: vec!["file".to_string(), "resolution".to_string()],
534 suggestion: Some("Run --help export to see valid keys for --output csv.".to_string()),
535 }
536 .into();
537
538 let formatted = format_error(&error);
539 assert!(formatted.contains("compression"));
540 assert!(formatted.contains("csv"));
541 assert!(formatted.contains("file"));
542 assert!(formatted.contains("resolution"));
543 assert!(formatted.contains("Run --help export"));
544 }
545
546 #[test]
547 fn test_format_parse_missing_required_option_parameter_with_suggestion() {
548 let error: DynamicCliError = ParseError::MissingRequiredOptionParameter {
549 option: "output".to_string(),
550 discriminant: "csv".to_string(),
551 key: "file".to_string(),
552 suggestion: Some(
553 "Run --help export to see required keys for --output csv.".to_string(),
554 ),
555 }
556 .into();
557
558 let formatted = format_error(&error);
559 assert!(formatted.contains("file"));
560 assert!(formatted.contains("csv"));
561 assert!(formatted.contains("Run --help export"));
562 }
563
564 #[test]
565 fn test_format_parse_unknown_discriminant_with_suggestion() {
566 let error: DynamicCliError = ParseError::UnknownDiscriminant {
567 option: "output".to_string(),
568 value: "xml".to_string(),
569 valid_choices: vec!["csv".to_string(), "plot".to_string()],
570 suggestion: Some("Run --help export to see valid --output kinds.".to_string()),
571 }
572 .into();
573
574 let formatted = format_error(&error);
575 assert!(formatted.contains("xml"));
576 assert!(formatted.contains("csv"));
577 assert!(formatted.contains("plot"));
578 assert!(formatted.contains("Run --help export"));
579 }
580
581 #[test]
582 fn test_format_parse_duplicate_option_occurrence_with_suggestion() {
583 let error: DynamicCliError = ParseError::DuplicateOptionOccurrence {
584 option: "output".to_string(),
585 discriminant: "csv".to_string(),
586 params: vec![("file".to_string(), "results.csv".to_string())],
587 suggestion: Some(
588 "Remove one of the two identical --output csv occurrences.".to_string(),
589 ),
590 }
591 .into();
592
593 let formatted = format_error(&error);
594 assert!(formatted.contains("results.csv"));
595 assert!(formatted.contains("csv"));
596 assert!(formatted.contains("Remove one of the two identical"));
597 }
598
599 #[test]
600 fn test_debug_new_parse_error_variants_dd024() {
601 let variants: Vec<ParseError> = vec![
604 ParseError::UnknownOptionParameter {
605 option: "output".to_string(),
606 discriminant: "csv".to_string(),
607 key: "compression".to_string(),
608 valid_keys: vec!["file".to_string()],
609 suggestion: None,
610 },
611 ParseError::MissingRequiredOptionParameter {
612 option: "output".to_string(),
613 discriminant: "csv".to_string(),
614 key: "file".to_string(),
615 suggestion: None,
616 },
617 ParseError::UnknownDiscriminant {
618 option: "output".to_string(),
619 value: "xml".to_string(),
620 valid_choices: vec!["csv".to_string()],
621 suggestion: None,
622 },
623 ParseError::DuplicateOptionOccurrence {
624 option: "output".to_string(),
625 discriminant: "csv".to_string(),
626 params: vec![("file".to_string(), "results.csv".to_string())],
627 suggestion: None,
628 },
629 ];
630
631 for variant in variants {
632 let debug_str = format!("{:?}", variant);
633 assert!(!debug_str.is_empty());
634 }
635 }
636
637 #[test]
638 fn test_format_parse_type_parse_error_shows_info_block() {
639 let error: DynamicCliError = ParseError::TypeParseError {
640 arg_name: "count".to_string(),
641 expected_type: "integer".to_string(),
642 value: "abc".to_string(),
643 details: None,
644 }
645 .into();
646
647 let formatted = format_error(&error);
648 assert!(formatted.contains("integer"));
649 assert!(formatted.contains("count"));
650 assert!(formatted.contains("abc"));
651 }
652
653 #[test]
656 fn test_format_validation_file_not_found_with_suggestion() {
657 let error: DynamicCliError = ValidationError::FileNotFound {
658 path: PathBuf::from("data.csv"),
659 arg_name: "input".to_string(),
660 suggestion: Some("Check that the file exists.".to_string()),
661 }
662 .into();
663
664 let formatted = format_error(&error);
665 assert!(formatted.contains("data.csv"));
666 assert!(formatted.contains("Check that the file exists."));
667 }
668
669 #[test]
670 fn test_format_validation_out_of_range_with_suggestion() {
671 let error: DynamicCliError = ValidationError::OutOfRange {
672 arg_name: "percentage".to_string(),
673 value: 150.0,
674 min: 0.0,
675 max: 100.0,
676 suggestion: Some("Value must be between 0 and 100.".to_string()),
677 }
678 .into();
679
680 let formatted = format_error(&error);
681 assert!(formatted.contains("percentage"));
682 assert!(formatted.contains("Value must be between 0 and 100."));
683 }
684
685 #[test]
686 fn test_format_validation_mutually_exclusive_with_suggestion() {
687 let error: DynamicCliError = ValidationError::MutuallyExclusive {
688 arg1: "--verbose".to_string(),
689 arg2: "--quiet".to_string(),
690 suggestion: Some("Remove one of the two conflicting options.".to_string()),
691 }
692 .into();
693
694 let formatted = format_error(&error);
695 assert!(formatted.contains("--verbose"));
696 assert!(formatted.contains("Remove one of the two conflicting options."));
697 }
698
699 #[test]
700 fn test_format_validation_missing_dependency_with_suggestion() {
701 let error: DynamicCliError = ValidationError::MissingDependency {
702 arg_name: "format".to_string(),
703 required_arg: "output".to_string(),
704 suggestion: Some("Add --output to your command.".to_string()),
705 }
706 .into();
707
708 let formatted = format_error(&error);
709 assert!(formatted.contains("format"));
710 assert!(formatted.contains("Add --output to your command."));
711 }
712
713 #[test]
714 fn test_format_validation_invalid_extension_no_suggestion_line() {
715 let error: DynamicCliError = ValidationError::InvalidExtension {
717 arg_name: "input".to_string(),
718 path: PathBuf::from("data.png"),
719 expected: vec![".csv".to_string(), ".tsv".to_string()],
720 }
721 .into();
722
723 let formatted = format_error(&error);
724 assert!(formatted.contains("data.png"));
725 assert!(!formatted.contains('ℹ'));
726 }
727
728 #[test]
731 fn test_format_execution_handler_not_found_with_suggestion() {
732 let error: DynamicCliError = ExecutionError::HandlerNotFound {
733 command: "run".to_string(),
734 implementation: "run_handler".to_string(),
735 suggestion: Some(
736 "Ensure .register_sync_handler(\"run_handler\", ...) was called.".to_string(),
737 ),
738 }
739 .into();
740
741 let formatted = format_error(&error);
742 assert!(formatted.contains("run"));
743 assert!(formatted.contains("run_handler"));
744 assert!(formatted.contains("register_sync_handler"));
745 }
746
747 #[test]
748 fn test_format_execution_context_downcast_failed_with_suggestion() {
749 let error: DynamicCliError = ExecutionError::ContextDowncastFailed {
750 expected_type: "MyCtx".to_string(),
751 suggestion: Some("Check the context type.".to_string()),
752 }
753 .into();
754
755 let formatted = format_error(&error);
756 assert!(formatted.contains("MyCtx"));
757 assert!(formatted.contains("Check the context type."));
758 }
759
760 #[test]
761 fn test_format_execution_interrupted_no_suggestion() {
762 let error: DynamicCliError = ExecutionError::Interrupted.into();
763 let formatted = format_error(&error);
764 assert!(formatted.contains("interrupted"));
765 assert!(!formatted.contains('ℹ'));
766 }
767
768 #[test]
771 fn test_format_registry_missing_handler_with_suggestion() {
772 let error: DynamicCliError = RegistryError::MissingHandler {
773 command: "export".to_string(),
774 suggestion: Some(
775 "Call .register_sync_handler(\"export\", ...) before running.".to_string(),
776 ),
777 }
778 .into();
779
780 let formatted = format_error(&error);
781 assert!(formatted.contains("export"));
782 assert!(formatted.contains("register_sync_handler"));
783 }
784
785 #[test]
786 fn test_format_registry_duplicate_registration_with_suggestion() {
787 let error: DynamicCliError = RegistryError::DuplicateRegistration {
788 name: "run".to_string(),
789 suggestion: Some("Command names must be unique.".to_string()),
790 }
791 .into();
792
793 let formatted = format_error(&error);
794 assert!(formatted.contains("run"));
795 assert!(formatted.contains("Command names must be unique."));
796 }
797
798 #[test]
799 fn test_format_registry_duplicate_alias_with_suggestion() {
800 let error: DynamicCliError = RegistryError::DuplicateAlias {
801 alias: "r".to_string(),
802 existing_command: "run".to_string(),
803 suggestion: Some("Choose a different alias.".to_string()),
804 }
805 .into();
806
807 let formatted = format_error(&error);
808 assert!(formatted.contains("run"));
809 assert!(formatted.contains("Choose a different alias."));
810 }
811
812 #[test]
813 fn test_format_registry_fault_tolerance_mismatch_with_suggestion() {
814 let error: DynamicCliError =
815 RegistryError::fault_tolerance_mismatch("solve", false, true).into();
816
817 let formatted = format_error(&error);
818 assert!(formatted.contains("solve"));
819 assert!(formatted.contains("continue_on_failure: false"));
820 }
821
822 #[cfg(feature = "wasm-plugins")]
825 #[test]
826 fn test_format_wasm_function_not_found_with_suggestion() {
827 let error: DynamicCliError = WasmError::FunctionNotFound {
828 function: "dcli_dealloc".to_string(),
829 module: "plugin.wasm".to_string(),
830 suggestion: Some("Export `dcli_dealloc(ptr: i32, size: i32)`.".to_string()),
831 }
832 .into();
833
834 let formatted = format_error(&error);
835 assert!(formatted.contains("dcli_dealloc"));
836 assert!(formatted.contains("Export `dcli_dealloc"));
837 }
838
839 #[cfg(feature = "wasm-plugins")]
840 #[test]
841 fn test_format_wasm_guest_error_without_suggestion_line() {
842 let error: DynamicCliError = WasmError::GuestError {
843 code: 1,
844 message: Some("invalid argument".to_string()),
845 }
846 .into();
847
848 let formatted = format_error(&error);
849 assert!(formatted.contains("invalid argument"));
850 assert!(!formatted.contains('ℹ'));
852 }
853
854 #[test]
857 fn test_display_error_does_not_panic() {
858 let error: DynamicCliError = ConfigError::FileNotFound {
859 path: PathBuf::from("test.yaml"),
860 suggestion: None,
861 }
862 .into();
863 display_error(&error);
865 }
866}