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 append_suggestion(output, suggestion.as_deref());
208 }
209
210 _ => {}
211 }
212}
213
214fn format_config_error(output: &mut String, error: &ConfigError) {
216 match error {
217 ConfigError::YamlParse {
218 source,
219 line,
220 column,
221 } => {
222 output.push_str(&format!("{}\n", source));
223 if let (Some(l), Some(c)) = (line, column) {
224 output.push_str(&format!(
225 " {} line {}, column {}\n",
226 color_dimmed("at"),
227 color_arg_name(&l.to_string()),
228 color_arg_name(&c.to_string())
229 ));
230 }
231 }
232
233 ConfigError::JsonParse {
234 source,
235 line,
236 column,
237 } => {
238 output.push_str(&format!("{}\n", source));
239 output.push_str(&format!(
240 " {} line {}, column {}\n",
241 color_dimmed("at"),
242 color_arg_name(&line.to_string()),
243 color_arg_name(&column.to_string())
244 ));
245 }
246
247 ConfigError::InvalidSchema {
248 reason,
249 path,
250 suggestion,
251 } => {
252 output.push_str(&format!("{}\n", reason));
253 if let Some(p) = path {
254 output.push_str(&format!(
255 " {} {}\n",
256 color_dimmed("in"),
257 color_type_name(p)
258 ));
259 }
260 append_suggestion(output, suggestion.as_deref());
261 }
262
263 ConfigError::FileNotFound { suggestion, .. }
264 | ConfigError::UnsupportedFormat { suggestion, .. }
265 | ConfigError::DuplicateCommand { suggestion, .. }
266 | ConfigError::UnknownType { suggestion, .. }
267 | ConfigError::Inconsistency { suggestion, .. } => {
268 output.push_str(&format!("{}\n", error));
269 append_suggestion(output, suggestion.as_deref());
270 }
271 }
272}
273
274fn format_validation_error(output: &mut String, error: &ValidationError) {
276 output.push_str(&format!("{}\n", error));
277
278 let suggestion = match error {
279 ValidationError::FileNotFound { suggestion, .. } => suggestion.as_deref(),
280 ValidationError::OutOfRange { suggestion, .. } => suggestion.as_deref(),
281 ValidationError::CustomConstraint { suggestion, .. } => suggestion.as_deref(),
282 ValidationError::MissingDependency { suggestion, .. } => suggestion.as_deref(),
283 ValidationError::MutuallyExclusive { suggestion, .. } => suggestion.as_deref(),
284 ValidationError::InvalidExtension { .. } => None,
286 };
287
288 append_suggestion(output, suggestion);
289}
290
291fn format_execution_error(output: &mut String, error: &ExecutionError) {
293 output.push_str(&format!("{}\n", error));
294
295 let suggestion = match error {
296 ExecutionError::HandlerNotFound { suggestion, .. } => suggestion.as_deref(),
297 ExecutionError::ContextDowncastFailed { suggestion, .. } => suggestion.as_deref(),
298 ExecutionError::InvalidContextState { suggestion, .. } => suggestion.as_deref(),
299 ExecutionError::CommandFailed(_) | ExecutionError::Interrupted => None,
301 };
302
303 append_suggestion(output, suggestion);
304}
305
306fn format_registry_error(output: &mut String, error: &RegistryError) {
308 output.push_str(&format!("{}\n", error));
309
310 let suggestion = match error {
311 RegistryError::DuplicateRegistration { suggestion, .. } => suggestion.as_deref(),
312 RegistryError::DuplicateAlias { suggestion, .. } => suggestion.as_deref(),
313 RegistryError::MissingHandler { suggestion, .. } => suggestion.as_deref(),
314 };
315
316 append_suggestion(output, suggestion);
317}
318
319#[cfg(feature = "wasm-plugins")]
326fn format_wasm_error(output: &mut String, error: &WasmError) {
327 output.push_str(&format!("{}\n", error));
328
329 let suggestion = match error {
330 WasmError::LoadFailed { suggestion, .. } => suggestion.as_deref(),
331 WasmError::FunctionNotFound { suggestion, .. } => suggestion.as_deref(),
332 WasmError::GuestError { .. } => None,
333 WasmError::SerializationFailed(_) => None,
334 WasmError::MemoryAccessFailed { .. } => None,
335 };
336
337 append_suggestion(output, suggestion);
338}
339
340fn append_suggestion(output: &mut String, suggestion: Option<&str>) {
354 if let Some(s) = suggestion {
355 output.push_str(&format!(" {} {}\n", color_info("ℹ"), s));
356 }
357}
358
359#[cfg(test)]
364mod tests {
365 use super::*;
366 use std::path::PathBuf;
367
368 #[test]
371 fn test_format_config_file_not_found_contains_path() {
372 let error: DynamicCliError = ConfigError::FileNotFound {
373 path: PathBuf::from("test.yaml"),
374 suggestion: None,
375 }
376 .into();
377
378 let formatted = format_error(&error);
379 assert!(formatted.contains("Error:"));
380 assert!(formatted.contains("test.yaml"));
381 }
382
383 #[test]
384 fn test_format_config_file_not_found_with_suggestion() {
385 let error: DynamicCliError = ConfigError::FileNotFound {
386 path: PathBuf::from("test.yaml"),
387 suggestion: Some("Verify the path.".to_string()),
388 }
389 .into();
390
391 let formatted = format_error(&error);
392 assert!(formatted.contains("Verify the path."));
393 }
394
395 #[test]
396 fn test_format_config_file_not_found_no_suggestion_no_hint_line() {
397 let error: DynamicCliError = ConfigError::FileNotFound {
398 path: PathBuf::from("test.yaml"),
399 suggestion: None,
400 }
401 .into();
402
403 let formatted = format_error(&error);
404 assert!(!formatted.contains('ℹ'));
406 }
407
408 #[test]
409 fn test_format_config_unsupported_format_with_suggestion() {
410 let error: DynamicCliError = ConfigError::UnsupportedFormat {
411 extension: ".toml".to_string(),
412 suggestion: Some("Use .yaml instead.".to_string()),
413 }
414 .into();
415
416 let formatted = format_error(&error);
417 assert!(formatted.contains(".toml"));
418 assert!(formatted.contains("Use .yaml instead."));
419 }
420
421 #[test]
422 fn test_format_config_yaml_parse_contains_location() {
423 let yaml_error = serde_yaml::from_str::<serde_yaml::Value>("invalid: [")
424 .err()
425 .unwrap();
426
427 let error: DynamicCliError = ConfigError::yaml_parse_with_location(yaml_error).into();
428 let formatted = format_error(&error);
429 assert!(formatted.contains("Error:"));
430 }
431
432 #[test]
433 fn test_format_config_invalid_schema_with_path_and_suggestion() {
434 let error: DynamicCliError = ConfigError::InvalidSchema {
435 reason: "missing field".to_string(),
436 path: Some("commands[0]".to_string()),
437 suggestion: Some("Add a name field.".to_string()),
438 }
439 .into();
440
441 let formatted = format_error(&error);
442 assert!(formatted.contains("missing field"));
443 assert!(formatted.contains("commands[0]"));
444 assert!(formatted.contains("Add a name field."));
445 }
446
447 #[test]
450 fn test_format_parse_unknown_command_with_suggestions() {
451 let error: DynamicCliError = ParseError::UnknownCommand {
452 command: "simulat".to_string(),
453 suggestions: vec!["simulate".to_string(), "validation".to_string()],
454 }
455 .into();
456
457 let formatted = format_error(&error);
458 assert!(formatted.contains("Unknown command"));
459 assert!(formatted.contains("simulat"));
460 assert!(formatted.contains("Did you mean"));
461 assert!(formatted.contains("simulate"));
462 }
463
464 #[test]
465 fn test_format_parse_unknown_command_no_suggestions() {
466 let error: DynamicCliError = ParseError::UnknownCommand {
467 command: "xyz".to_string(),
468 suggestions: vec![],
469 }
470 .into();
471
472 let formatted = format_error(&error);
473 assert!(formatted.contains("xyz"));
474 assert!(!formatted.contains("Did you mean"));
475 }
476
477 #[test]
478 fn test_format_parse_missing_argument_with_suggestion() {
479 let error: DynamicCliError = ParseError::MissingArgument {
480 argument: "file".to_string(),
481 command: "process".to_string(),
482 suggestion: Some("Run --help process to see required arguments.".to_string()),
483 }
484 .into();
485
486 let formatted = format_error(&error);
487 assert!(formatted.contains("file"));
488 assert!(formatted.contains("Run --help process"));
489 }
490
491 #[test]
492 fn test_format_parse_missing_option_with_suggestion() {
493 let error: DynamicCliError = ParseError::MissingOption {
494 option: "output".to_string(),
495 command: "export".to_string(),
496 suggestion: Some("Run --help export to see required options.".to_string()),
497 }
498 .into();
499
500 let formatted = format_error(&error);
501 assert!(formatted.contains("output"));
502 assert!(formatted.contains("Run --help export"));
503 }
504
505 #[test]
506 fn test_format_parse_too_many_arguments_with_suggestion() {
507 let error: DynamicCliError = ParseError::TooManyArguments {
508 command: "run".to_string(),
509 expected: 1,
510 got: 3,
511 suggestion: Some("Run --help run for the expected usage.".to_string()),
512 }
513 .into();
514
515 let formatted = format_error(&error);
516 assert!(formatted.contains("run"));
517 assert!(formatted.contains("Run --help run"));
518 }
519
520 #[test]
521 fn test_format_parse_type_parse_error_shows_info_block() {
522 let error: DynamicCliError = ParseError::TypeParseError {
523 arg_name: "count".to_string(),
524 expected_type: "integer".to_string(),
525 value: "abc".to_string(),
526 details: None,
527 }
528 .into();
529
530 let formatted = format_error(&error);
531 assert!(formatted.contains("integer"));
532 assert!(formatted.contains("count"));
533 assert!(formatted.contains("abc"));
534 }
535
536 #[test]
539 fn test_format_validation_file_not_found_with_suggestion() {
540 let error: DynamicCliError = ValidationError::FileNotFound {
541 path: PathBuf::from("data.csv"),
542 arg_name: "input".to_string(),
543 suggestion: Some("Check that the file exists.".to_string()),
544 }
545 .into();
546
547 let formatted = format_error(&error);
548 assert!(formatted.contains("data.csv"));
549 assert!(formatted.contains("Check that the file exists."));
550 }
551
552 #[test]
553 fn test_format_validation_out_of_range_with_suggestion() {
554 let error: DynamicCliError = ValidationError::OutOfRange {
555 arg_name: "percentage".to_string(),
556 value: 150.0,
557 min: 0.0,
558 max: 100.0,
559 suggestion: Some("Value must be between 0 and 100.".to_string()),
560 }
561 .into();
562
563 let formatted = format_error(&error);
564 assert!(formatted.contains("percentage"));
565 assert!(formatted.contains("Value must be between 0 and 100."));
566 }
567
568 #[test]
569 fn test_format_validation_mutually_exclusive_with_suggestion() {
570 let error: DynamicCliError = ValidationError::MutuallyExclusive {
571 arg1: "--verbose".to_string(),
572 arg2: "--quiet".to_string(),
573 suggestion: Some("Remove one of the two conflicting options.".to_string()),
574 }
575 .into();
576
577 let formatted = format_error(&error);
578 assert!(formatted.contains("--verbose"));
579 assert!(formatted.contains("Remove one of the two conflicting options."));
580 }
581
582 #[test]
583 fn test_format_validation_missing_dependency_with_suggestion() {
584 let error: DynamicCliError = ValidationError::MissingDependency {
585 arg_name: "format".to_string(),
586 required_arg: "output".to_string(),
587 suggestion: Some("Add --output to your command.".to_string()),
588 }
589 .into();
590
591 let formatted = format_error(&error);
592 assert!(formatted.contains("format"));
593 assert!(formatted.contains("Add --output to your command."));
594 }
595
596 #[test]
597 fn test_format_validation_invalid_extension_no_suggestion_line() {
598 let error: DynamicCliError = ValidationError::InvalidExtension {
600 arg_name: "input".to_string(),
601 path: PathBuf::from("data.png"),
602 expected: vec![".csv".to_string(), ".tsv".to_string()],
603 }
604 .into();
605
606 let formatted = format_error(&error);
607 assert!(formatted.contains("data.png"));
608 assert!(!formatted.contains('ℹ'));
609 }
610
611 #[test]
614 fn test_format_execution_handler_not_found_with_suggestion() {
615 let error: DynamicCliError = ExecutionError::HandlerNotFound {
616 command: "run".to_string(),
617 implementation: "run_handler".to_string(),
618 suggestion: Some(
619 "Ensure .register_sync_handler(\"run_handler\", ...) was called.".to_string(),
620 ),
621 }
622 .into();
623
624 let formatted = format_error(&error);
625 assert!(formatted.contains("run"));
626 assert!(formatted.contains("run_handler"));
627 assert!(formatted.contains("register_sync_handler"));
628 }
629
630 #[test]
631 fn test_format_execution_context_downcast_failed_with_suggestion() {
632 let error: DynamicCliError = ExecutionError::ContextDowncastFailed {
633 expected_type: "MyCtx".to_string(),
634 suggestion: Some("Check the context type.".to_string()),
635 }
636 .into();
637
638 let formatted = format_error(&error);
639 assert!(formatted.contains("MyCtx"));
640 assert!(formatted.contains("Check the context type."));
641 }
642
643 #[test]
644 fn test_format_execution_interrupted_no_suggestion() {
645 let error: DynamicCliError = ExecutionError::Interrupted.into();
646 let formatted = format_error(&error);
647 assert!(formatted.contains("interrupted"));
648 assert!(!formatted.contains('ℹ'));
649 }
650
651 #[test]
654 fn test_format_registry_missing_handler_with_suggestion() {
655 let error: DynamicCliError = RegistryError::MissingHandler {
656 command: "export".to_string(),
657 suggestion: Some(
658 "Call .register_sync_handler(\"export\", ...) before running.".to_string(),
659 ),
660 }
661 .into();
662
663 let formatted = format_error(&error);
664 assert!(formatted.contains("export"));
665 assert!(formatted.contains("register_sync_handler"));
666 }
667
668 #[test]
669 fn test_format_registry_duplicate_registration_with_suggestion() {
670 let error: DynamicCliError = RegistryError::DuplicateRegistration {
671 name: "run".to_string(),
672 suggestion: Some("Command names must be unique.".to_string()),
673 }
674 .into();
675
676 let formatted = format_error(&error);
677 assert!(formatted.contains("run"));
678 assert!(formatted.contains("Command names must be unique."));
679 }
680
681 #[test]
682 fn test_format_registry_duplicate_alias_with_suggestion() {
683 let error: DynamicCliError = RegistryError::DuplicateAlias {
684 alias: "r".to_string(),
685 existing_command: "run".to_string(),
686 suggestion: Some("Choose a different alias.".to_string()),
687 }
688 .into();
689
690 let formatted = format_error(&error);
691 assert!(formatted.contains("run"));
692 assert!(formatted.contains("Choose a different alias."));
693 }
694
695 #[cfg(feature = "wasm-plugins")]
698 #[test]
699 fn test_format_wasm_function_not_found_with_suggestion() {
700 let error: DynamicCliError = WasmError::FunctionNotFound {
701 function: "dcli_dealloc".to_string(),
702 module: "plugin.wasm".to_string(),
703 suggestion: Some("Export `dcli_dealloc(ptr: i32, size: i32)`.".to_string()),
704 }
705 .into();
706
707 let formatted = format_error(&error);
708 assert!(formatted.contains("dcli_dealloc"));
709 assert!(formatted.contains("Export `dcli_dealloc"));
710 }
711
712 #[cfg(feature = "wasm-plugins")]
713 #[test]
714 fn test_format_wasm_guest_error_without_suggestion_line() {
715 let error: DynamicCliError = WasmError::GuestError {
716 code: 1,
717 message: Some("invalid argument".to_string()),
718 }
719 .into();
720
721 let formatted = format_error(&error);
722 assert!(formatted.contains("invalid argument"));
723 assert!(!formatted.contains('ℹ'));
725 }
726
727 #[test]
730 fn test_display_error_does_not_panic() {
731 let error: DynamicCliError = ConfigError::FileNotFound {
732 path: PathBuf::from("test.yaml"),
733 suggestion: None,
734 }
735 .into();
736 display_error(&error);
738 }
739}