dynamic_cli/error/types.rs
1//! Error types for dynamic-cli
2//!
3//! Defines all possible error types with context and clear messages.
4//!
5//! Each error variant carries an optional `suggestion` field that surfaces
6//! an actionable hint to the end user. Suggestions are rendered by
7//! [`crate::error::display::format_error`] and are never part of the
8//! `Display` string itself, keeping machine-readable messages stable.
9
10use std::path::PathBuf;
11use thiserror::Error;
12
13/// Main error for the dynamic-cli framework
14///
15/// Encompasses all possible error categories. Uses `thiserror`
16/// to automatically generate `Display` and `Error` implementations.
17#[derive(Debug, Error)]
18pub enum DynamicCliError {
19 /// Errors related to the configuration file
20 #[error(transparent)]
21 Config(#[from] ConfigError),
22
23 /// Command parsing errors
24 #[error(transparent)]
25 Parse(#[from] ParseError),
26
27 /// Validation errors
28 #[error(transparent)]
29 Validation(#[from] ValidationError),
30
31 /// Execution errors
32 #[error(transparent)]
33 Execution(#[from] ExecutionError),
34
35 /// Registry errors
36 #[error(transparent)]
37 Registry(#[from] RegistryError),
38
39 /// WASM plugin errors (only available with the `wasm-plugins` feature)
40 #[cfg(feature = "wasm-plugins")]
41 #[error(transparent)]
42 Wasm(#[from] WasmError),
43
44 /// I/O errors
45 #[error("I/O error: {0}")]
46 Io(#[from] std::io::Error),
47}
48
49// ═══════════════════════════════════════════════════════════
50// CONFIGURATION ERRORS
51// ═══════════════════════════════════════════════════════════
52
53/// Errors related to loading and parsing the configuration file
54///
55/// These errors occur when loading the `commands.yaml` or `commands.json`
56/// file and its structural validation.
57#[derive(Debug, Error)]
58pub enum ConfigError {
59 /// Configuration file not found
60 ///
61 /// # Example
62 ///
63 /// ```
64 /// use dynamic_cli::error::ConfigError;
65 /// use std::path::PathBuf;
66 ///
67 /// let error = ConfigError::FileNotFound {
68 /// path: PathBuf::from("missing.yaml"),
69 /// suggestion: Some("Verify the path and file permissions.".to_string()),
70 /// };
71 /// let msg = format!("{}", error);
72 /// assert!(msg.contains("missing.yaml"));
73 /// ```
74 #[error("Configuration file not found: {path:?}")]
75 FileNotFound {
76 path: PathBuf,
77 /// Actionable hint surfaced to the user (not part of the Display string)
78 suggestion: Option<String>,
79 },
80
81 /// Unsupported file extension
82 ///
83 /// Only `.yaml`, `.yml` and `.json` are supported.
84 ///
85 /// # Example
86 ///
87 /// ```
88 /// use dynamic_cli::error::ConfigError;
89 ///
90 /// let error = ConfigError::UnsupportedFormat {
91 /// extension: ".toml".to_string(),
92 /// suggestion: Some("Rename the file with a .yaml, .yml or .json extension.".to_string()),
93 /// };
94 /// let msg = format!("{}", error);
95 /// assert!(msg.contains(".toml"));
96 /// ```
97 #[error("Unsupported file format: '{extension}'. Supported: .yaml, .yml, .json")]
98 UnsupportedFormat {
99 extension: String,
100 /// Actionable hint surfaced to the user (not part of the Display string)
101 suggestion: Option<String>,
102 },
103
104 /// YAML parsing error
105 #[error("Failed to parse YAML configuration at line {line:?}, column {column:?}: {source}")]
106 YamlParse {
107 #[source]
108 source: serde_yaml::Error,
109 /// Position in the file (if available)
110 line: Option<usize>,
111 column: Option<usize>,
112 },
113
114 /// JSON parsing error
115 #[error("Failed to parse JSON configuration at line {line}, column {column}: {source}")]
116 JsonParse {
117 #[source]
118 source: serde_json::Error,
119 /// Position in the file
120 line: usize,
121 column: usize,
122 },
123
124 /// Invalid configuration schema
125 ///
126 /// The file structure doesn't match the expected format.
127 ///
128 /// # Example
129 ///
130 /// ```
131 /// use dynamic_cli::error::ConfigError;
132 ///
133 /// let error = ConfigError::InvalidSchema {
134 /// reason: "Missing required field 'name'".to_string(),
135 /// path: Some("commands[0]".to_string()),
136 /// suggestion: Some("Add a 'name' field to each command entry.".to_string()),
137 /// };
138 /// let msg = format!("{}", error);
139 /// assert!(msg.contains("Missing required field"));
140 /// ```
141 #[error("Invalid configuration schema: {reason} (at {path:?})")]
142 InvalidSchema {
143 reason: String,
144 /// Path in the config (e.g., "commands[0].options[2].type")
145 path: Option<String>,
146 /// Actionable hint surfaced to the user (not part of the Display string)
147 suggestion: Option<String>,
148 },
149
150 /// Duplicate command (same name or alias)
151 ///
152 /// # Example
153 ///
154 /// ```
155 /// use dynamic_cli::error::ConfigError;
156 ///
157 /// let error = ConfigError::DuplicateCommand {
158 /// name: "run".to_string(),
159 /// suggestion: Some("Rename one of the conflicting commands or aliases.".to_string()),
160 /// };
161 /// let msg = format!("{}", error);
162 /// assert!(msg.contains("run"));
163 /// ```
164 #[error("Duplicate command name or alias: '{name}'")]
165 DuplicateCommand {
166 name: String,
167 /// Actionable hint surfaced to the user (not part of the Display string)
168 suggestion: Option<String>,
169 },
170
171 /// Unknown argument type
172 ///
173 /// # Example
174 ///
175 /// ```
176 /// use dynamic_cli::error::ConfigError;
177 ///
178 /// let error = ConfigError::UnknownType {
179 /// type_name: "datetime".to_string(),
180 /// context: "commands[1].options[0]".to_string(),
181 /// suggestion: Some(
182 /// "Supported types: string, integer, float, boolean, file.".to_string()
183 /// ),
184 /// };
185 /// let msg = format!("{}", error);
186 /// assert!(msg.contains("datetime"));
187 /// ```
188 #[error("Unknown argument type: '{type_name}' in {context}")]
189 UnknownType {
190 type_name: String,
191 context: String,
192 /// Actionable hint surfaced to the user (not part of the Display string)
193 suggestion: Option<String>,
194 },
195
196 /// Inconsistent configuration
197 ///
198 /// For example, a default value that's not in the allowed choices.
199 ///
200 /// # Example
201 ///
202 /// ```
203 /// use dynamic_cli::error::ConfigError;
204 ///
205 /// let error = ConfigError::Inconsistency {
206 /// details: "Default value 'fast' is not in choices: slow, medium".to_string(),
207 /// suggestion: Some("Ensure the default value matches one of the allowed choices.".to_string()),
208 /// };
209 /// let msg = format!("{}", error);
210 /// assert!(msg.contains("Default value"));
211 /// ```
212 #[error("Configuration inconsistency: {details}")]
213 Inconsistency {
214 details: String,
215 /// Actionable hint surfaced to the user (not part of the Display string)
216 suggestion: Option<String>,
217 },
218}
219
220// ═══════════════════════════════════════════════════════════
221// PARSING ERRORS
222// ═══════════════════════════════════════════════════════════
223
224/// Errors when parsing user commands
225///
226/// These errors occur when analyzing arguments provided
227/// by the user in CLI or REPL mode.
228#[derive(Debug, Error)]
229pub enum ParseError {
230 /// Unknown command
231 ///
232 /// The user typed a command that doesn't exist.
233 /// Includes suggestions based on Levenshtein distance.
234 #[error("Unknown command: '{command}'. Type 'help' for available commands.")]
235 UnknownCommand {
236 command: String,
237 /// Similar command suggestions (from Levenshtein distance)
238 suggestions: Vec<String>,
239 },
240
241 /// Missing required positional argument
242 ///
243 /// # Example
244 ///
245 /// ```
246 /// use dynamic_cli::error::ParseError;
247 ///
248 /// let error = ParseError::MissingArgument {
249 /// argument: "filename".to_string(),
250 /// command: "process".to_string(),
251 /// suggestion: Some("Run --help process to see required arguments.".to_string()),
252 /// };
253 /// let msg = format!("{}", error);
254 /// assert!(msg.contains("filename"));
255 /// ```
256 #[error("Missing required argument: {argument} for command '{command}'")]
257 MissingArgument {
258 argument: String,
259 command: String,
260 /// Actionable hint surfaced to the user (not part of the Display string)
261 suggestion: Option<String>,
262 },
263
264 /// Missing required option
265 ///
266 /// # Example
267 ///
268 /// ```
269 /// use dynamic_cli::error::ParseError;
270 ///
271 /// let error = ParseError::MissingOption {
272 /// option: "output".to_string(),
273 /// command: "export".to_string(),
274 /// suggestion: Some("Run --help export to see required options.".to_string()),
275 /// };
276 /// let msg = format!("{}", error);
277 /// assert!(msg.contains("output"));
278 /// ```
279 #[error("Missing required option: --{option} for command '{command}'")]
280 MissingOption {
281 option: String,
282 command: String,
283 /// Actionable hint surfaced to the user (not part of the Display string)
284 suggestion: Option<String>,
285 },
286
287 /// Too many positional arguments
288 ///
289 /// # Example
290 ///
291 /// ```
292 /// use dynamic_cli::error::ParseError;
293 ///
294 /// let error = ParseError::TooManyArguments {
295 /// command: "run".to_string(),
296 /// expected: 1,
297 /// got: 3,
298 /// suggestion: Some("Run --help run for the expected usage.".to_string()),
299 /// };
300 /// let msg = format!("{}", error);
301 /// assert!(msg.contains("run"));
302 /// ```
303 #[error("Too many arguments for command '{command}'. Expected {expected}, got {got}")]
304 TooManyArguments {
305 command: String,
306 expected: usize,
307 got: usize,
308 /// Actionable hint surfaced to the user (not part of the Display string)
309 suggestion: Option<String>,
310 },
311
312 /// Unknown option
313 ///
314 /// Includes similar option suggestions.
315 #[error("Unknown option: {flag} for command '{command}'")]
316 UnknownOption {
317 flag: String,
318 command: String,
319 /// Similar option suggestions (from Levenshtein distance)
320 suggestions: Vec<String>,
321 },
322
323 /// Type parsing error
324 ///
325 /// The user provided a value that can't be converted
326 /// to the expected type (e.g., "abc" for an integer).
327 #[error("Failed to parse {arg_name} as {expected_type}: '{value}'{}",
328 .details.as_ref().map(|d| format!(" ({})", d)).unwrap_or_default())]
329 TypeParseError {
330 arg_name: String,
331 expected_type: String,
332 value: String,
333 /// Error details (e.g., "not a valid integer")
334 details: Option<String>,
335 },
336
337 /// Value not in allowed choices
338 #[error("Invalid value for {arg_name}: '{value}'. Must be one of: {}",
339 .choices.join(", "))]
340 InvalidChoice {
341 arg_name: String,
342 value: String,
343 choices: Vec<String>,
344 },
345
346 /// Invalid command syntax
347 #[error("Invalid command syntax: {details}{}",
348 .hint.as_ref().map(|h| format!("\nHint: {}", h)).unwrap_or_default())]
349 InvalidSyntax {
350 details: String,
351 /// Example of correct syntax
352 hint: Option<String>,
353 },
354}
355
356// ═══════════════════════════════════════════════════════════
357// VALIDATION ERRORS
358// ═══════════════════════════════════════════════════════════
359
360/// Errors during argument validation
361///
362/// These errors occur after parsing, during validation
363/// of constraints defined in the configuration.
364#[derive(Debug, Error)]
365pub enum ValidationError {
366 /// Required file doesn't exist
367 ///
368 /// # Example
369 ///
370 /// ```
371 /// use dynamic_cli::error::ValidationError;
372 /// use std::path::PathBuf;
373 ///
374 /// let error = ValidationError::FileNotFound {
375 /// path: PathBuf::from("data.csv"),
376 /// arg_name: "input".to_string(),
377 /// suggestion: Some("Check that the file exists and the path is correct.".to_string()),
378 /// };
379 /// let msg = format!("{}", error);
380 /// assert!(msg.contains("data.csv"));
381 /// ```
382 #[error("File not found for argument '{arg_name}': {path:?}")]
383 FileNotFound {
384 path: PathBuf,
385 arg_name: String,
386 /// Actionable hint surfaced to the user (not part of the Display string)
387 suggestion: Option<String>,
388 },
389
390 /// Invalid file extension
391 #[error("Invalid file extension for {arg_name}: {path:?}. Expected: {}",
392 .expected.join(", "))]
393 InvalidExtension {
394 arg_name: String,
395 path: PathBuf,
396 expected: Vec<String>,
397 },
398
399 /// Value out of allowed range
400 ///
401 /// # Example
402 ///
403 /// ```
404 /// use dynamic_cli::error::ValidationError;
405 ///
406 /// let error = ValidationError::OutOfRange {
407 /// arg_name: "percentage".to_string(),
408 /// value: 150.0,
409 /// min: 0.0,
410 /// max: 100.0,
411 /// suggestion: Some("Value must be between 0 and 100.".to_string()),
412 /// };
413 /// let msg = format!("{}", error);
414 /// assert!(msg.contains("150"));
415 /// ```
416 #[error("{arg_name} must be between {min} and {max}, got {value}")]
417 OutOfRange {
418 arg_name: String,
419 value: f64,
420 min: f64,
421 max: f64,
422 /// Actionable hint surfaced to the user (not part of the Display string)
423 suggestion: Option<String>,
424 },
425
426 /// Custom constraint not met
427 ///
428 /// # Example
429 ///
430 /// ```
431 /// use dynamic_cli::error::ValidationError;
432 ///
433 /// let error = ValidationError::CustomConstraint {
434 /// arg_name: "email".to_string(),
435 /// reason: "not a valid email address".to_string(),
436 /// suggestion: Some("Provide a valid email address (e.g. user@example.com).".to_string()),
437 /// };
438 /// let msg = format!("{}", error);
439 /// assert!(msg.contains("email"));
440 /// ```
441 #[error("Validation failed for {arg_name}: {reason}")]
442 CustomConstraint {
443 arg_name: String,
444 reason: String,
445 /// Actionable hint surfaced to the user (not part of the Display string)
446 suggestion: Option<String>,
447 },
448
449 /// Dependency between arguments not satisfied
450 ///
451 /// Some arguments require the presence of other arguments.
452 ///
453 /// # Example
454 ///
455 /// ```
456 /// use dynamic_cli::error::ValidationError;
457 ///
458 /// let error = ValidationError::MissingDependency {
459 /// arg_name: "output-format".to_string(),
460 /// required_arg: "output".to_string(),
461 /// suggestion: Some("Add --output to your command.".to_string()),
462 /// };
463 /// let msg = format!("{}", error);
464 /// assert!(msg.contains("output-format"));
465 /// ```
466 #[error("{arg_name} requires {required_arg} to be specified")]
467 MissingDependency {
468 arg_name: String,
469 required_arg: String,
470 /// Actionable hint surfaced to the user (not part of the Display string)
471 suggestion: Option<String>,
472 },
473
474 /// Mutually exclusive arguments
475 ///
476 /// Some arguments cannot be used together.
477 ///
478 /// # Example
479 ///
480 /// ```
481 /// use dynamic_cli::error::ValidationError;
482 ///
483 /// let error = ValidationError::MutuallyExclusive {
484 /// arg1: "--verbose".to_string(),
485 /// arg2: "--quiet".to_string(),
486 /// suggestion: Some("Remove one of the two conflicting options.".to_string()),
487 /// };
488 /// let msg = format!("{}", error);
489 /// assert!(msg.contains("--verbose"));
490 /// ```
491 #[error("Options {arg1} and {arg2} cannot be used together")]
492 MutuallyExclusive {
493 arg1: String,
494 arg2: String,
495 /// Actionable hint surfaced to the user (not part of the Display string)
496 suggestion: Option<String>,
497 },
498}
499
500// ═══════════════════════════════════════════════════════════
501// EXECUTION ERRORS
502// ═══════════════════════════════════════════════════════════
503
504/// Errors during command execution
505///
506/// These errors occur during user code execution.
507#[derive(Debug, Error)]
508pub enum ExecutionError {
509 /// Command handler not found
510 ///
511 /// The implementation name in the config doesn't match any
512 /// registered handler.
513 ///
514 /// # Example
515 ///
516 /// ```
517 /// use dynamic_cli::error::ExecutionError;
518 ///
519 /// let error = ExecutionError::HandlerNotFound {
520 /// command: "run".to_string(),
521 /// implementation: "run_handler".to_string(),
522 /// suggestion: Some(
523 /// "Ensure .register_sync_handler(\"run_handler\", ...) was called before running."
524 /// .to_string()
525 /// ),
526 /// };
527 /// let msg = format!("{}", error);
528 /// assert!(msg.contains("run"));
529 /// ```
530 #[error("No handler registered for command '{command}' (implementation: '{implementation}')")]
531 HandlerNotFound {
532 command: String,
533 implementation: String,
534 /// Actionable hint surfaced to the user (not part of the Display string)
535 suggestion: Option<String>,
536 },
537
538 /// Error during context downcasting
539 ///
540 /// The handler tried to downcast the context to an incorrect type.
541 ///
542 /// # Example
543 ///
544 /// ```
545 /// use dynamic_cli::error::ExecutionError;
546 ///
547 /// let error = ExecutionError::ContextDowncastFailed {
548 /// expected_type: "MyAppContext".to_string(),
549 /// suggestion: Some(
550 /// "Check that the context type passed to the handler matches the expected type."
551 /// .to_string()
552 /// ),
553 /// };
554 /// let msg = format!("{}", error);
555 /// assert!(msg.contains("MyAppContext"));
556 /// ```
557 #[error("Failed to downcast execution context to expected type: {expected_type}")]
558 ContextDowncastFailed {
559 expected_type: String,
560 /// Actionable hint surfaced to the user (not part of the Display string)
561 suggestion: Option<String>,
562 },
563
564 /// Invalid context state for this operation
565 ///
566 /// # Example
567 ///
568 /// ```
569 /// use dynamic_cli::error::ExecutionError;
570 ///
571 /// let error = ExecutionError::InvalidContextState {
572 /// reason: "connection pool not initialised".to_string(),
573 /// suggestion: Some("Ensure the context is fully initialised before running commands.".to_string()),
574 /// };
575 /// let msg = format!("{}", error);
576 /// assert!(msg.contains("connection pool"));
577 /// ```
578 #[error("Invalid context state: {reason}")]
579 InvalidContextState {
580 reason: String,
581 /// Actionable hint surfaced to the user (not part of the Display string)
582 suggestion: Option<String>,
583 },
584
585 /// Error in command implementation
586 ///
587 /// Wraps errors from user code.
588 #[error("Command execution failed: {0}")]
589 CommandFailed(#[source] anyhow::Error),
590
591 /// Command interrupted by user
592 ///
593 /// User pressed Ctrl+C during execution.
594 #[error("Command interrupted by user")]
595 Interrupted,
596}
597
598// ═══════════════════════════════════════════════════════════
599// REGISTRY ERRORS
600// ═══════════════════════════════════════════════════════════
601
602/// Errors related to the command registry
603///
604/// These errors occur when registering commands
605/// and handlers in the registry.
606#[derive(Debug, Error)]
607pub enum RegistryError {
608 /// Attempt to register an already existing command
609 ///
610 /// # Example
611 ///
612 /// ```
613 /// use dynamic_cli::error::RegistryError;
614 ///
615 /// let error = RegistryError::DuplicateRegistration {
616 /// name: "run".to_string(),
617 /// suggestion: Some("Command names must be unique across the registry.".to_string()),
618 /// };
619 /// let msg = format!("{}", error);
620 /// assert!(msg.contains("run"));
621 /// ```
622 #[error("Command '{name}' is already registered")]
623 DuplicateRegistration {
624 name: String,
625 /// Actionable hint surfaced to the user (not part of the Display string)
626 suggestion: Option<String>,
627 },
628
629 /// Alias already used by another command
630 ///
631 /// # Example
632 ///
633 /// ```
634 /// use dynamic_cli::error::RegistryError;
635 ///
636 /// let error = RegistryError::DuplicateAlias {
637 /// alias: "r".to_string(),
638 /// existing_command: "run".to_string(),
639 /// suggestion: Some("Choose a different alias for one of the commands.".to_string()),
640 /// };
641 /// let msg = format!("{}", error);
642 /// assert!(msg.contains("run"));
643 /// ```
644 #[error("Alias '{alias}' is already used by command '{existing_command}'")]
645 DuplicateAlias {
646 alias: String,
647 existing_command: String,
648 /// Actionable hint surfaced to the user (not part of the Display string)
649 suggestion: Option<String>,
650 },
651
652 /// Missing handler for a definition
653 ///
654 /// A command is defined in the config but no handler
655 /// has been registered for it.
656 ///
657 /// # Example
658 ///
659 /// ```
660 /// use dynamic_cli::error::RegistryError;
661 ///
662 /// let error = RegistryError::MissingHandler {
663 /// command: "export".to_string(),
664 /// suggestion: Some(
665 /// "Call .register_sync_handler(\"export\", ...) before running.".to_string()
666 /// ),
667 /// };
668 /// let msg = format!("{}", error);
669 /// assert!(msg.contains("export"));
670 /// ```
671 #[error("No handler provided for command '{command}'")]
672 MissingHandler {
673 command: String,
674 /// Actionable hint surfaced to the user (not part of the Display string)
675 suggestion: Option<String>,
676 },
677}
678
679// ═══════════════════════════════════════════════════════════
680// WASM PLUGIN ERRORS (feature = "wasm-plugins")
681// ═══════════════════════════════════════════════════════════
682
683/// Errors related to loading and executing WASM plugins
684///
685/// Only available when the `wasm-plugins` feature is enabled.
686/// These errors occur when loading a `.wasm` module, validating its
687/// mandatory exports, or invoking a guest function across the host/guest
688/// boundary. See `WASM_PLUGIN_INTERFACE.md` for the full ABI contract.
689#[cfg(feature = "wasm-plugins")]
690#[derive(Debug, Error)]
691pub enum WasmError {
692 /// The `.wasm` module could not be loaded or instantiated
693 ///
694 /// # Example
695 ///
696 /// ```
697 /// use dynamic_cli::error::WasmError;
698 /// use std::path::PathBuf;
699 ///
700 /// let error = WasmError::LoadFailed {
701 /// path: PathBuf::from("plugin.wasm"),
702 /// source: anyhow::anyhow!("invalid magic number"),
703 /// suggestion: Some("Verify the file is a valid WASM binary.".to_string()),
704 /// };
705 /// let msg = format!("{}", error);
706 /// assert!(msg.contains("plugin.wasm"));
707 /// ```
708 #[error("Failed to load WASM module: {path:?}: {source}")]
709 LoadFailed {
710 path: PathBuf,
711 #[source]
712 source: anyhow::Error,
713 /// Actionable hint surfaced to the user (not part of the Display string)
714 suggestion: Option<String>,
715 },
716
717 /// A mandatory or mapped export is missing from the module
718 ///
719 /// Mandatory exports are `memory`, `dcli_alloc`, `dcli_dealloc`, and the
720 /// mapped business function. See `WASM_PLUGIN_INTERFACE.md`.
721 ///
722 /// # Example
723 ///
724 /// ```
725 /// use dynamic_cli::error::WasmError;
726 ///
727 /// let error = WasmError::FunctionNotFound {
728 /// function: "dcli_dealloc".to_string(),
729 /// module: "plugin.wasm".to_string(),
730 /// suggestion: Some(
731 /// "Export `dcli_dealloc(ptr: i32, size: i32)` from the WASM module.".to_string()
732 /// ),
733 /// };
734 /// let msg = format!("{}", error);
735 /// assert!(msg.contains("dcli_dealloc"));
736 /// ```
737 #[error("WASM function '{function}' not found in module '{module}'")]
738 FunctionNotFound {
739 function: String,
740 module: String,
741 /// Actionable hint surfaced to the user (not part of the Display string)
742 suggestion: Option<String>,
743 },
744
745 /// The guest function returned a non-zero error code
746 ///
747 /// `message` is populated from `dcli_last_error_message()` when the
748 /// module exports it; otherwise `None`.
749 ///
750 /// # Example
751 ///
752 /// ```
753 /// use dynamic_cli::error::WasmError;
754 ///
755 /// let error = WasmError::GuestError {
756 /// code: 1,
757 /// message: Some("invalid argument".to_string()),
758 /// };
759 /// let msg = format!("{}", error);
760 /// assert!(msg.contains('1'));
761 /// ```
762 #[error("WASM guest returned error code {code}{}",
763 .message.as_ref().map(|m| format!(": {m}")).unwrap_or_default())]
764 GuestError {
765 code: i32,
766 /// Detailed message from `dcli_last_error_message()`, if exported
767 message: Option<String>,
768 },
769
770 /// Failed to serialize handler arguments before crossing the WASM boundary
771 ///
772 /// # Example
773 ///
774 /// ```
775 /// use dynamic_cli::error::WasmError;
776 ///
777 /// let error = WasmError::SerializationFailed("unsupported map key type".to_string());
778 /// let msg = format!("{}", error);
779 /// assert!(msg.contains("unsupported map key type"));
780 /// ```
781 #[error("Failed to serialize arguments for WASM call: {0}")]
782 SerializationFailed(String),
783
784 /// Failed to read from or write to the guest's linear memory
785 ///
786 /// # Example
787 ///
788 /// ```
789 /// use dynamic_cli::error::WasmError;
790 ///
791 /// let error = WasmError::MemoryAccessFailed {
792 /// reason: "write out of bounds".to_string(),
793 /// };
794 /// let msg = format!("{}", error);
795 /// assert!(msg.contains("out of bounds"));
796 /// ```
797 #[error("Failed to access WASM guest memory: {reason}")]
798 MemoryAccessFailed { reason: String },
799}
800
801// ═══════════════════════════════════════════════════════════
802// HELPERS FOR CREATING CONTEXTUAL ERRORS
803// ═══════════════════════════════════════════════════════════
804
805impl ParseError {
806 /// Create an unknown command error with Levenshtein suggestions
807 ///
808 /// Automatically computes similar command names from the available list.
809 ///
810 /// # Arguments
811 ///
812 /// * `command` - The command typed by the user
813 /// * `available` - List of available commands
814 ///
815 /// # Example
816 ///
817 /// ```
818 /// use dynamic_cli::error::ParseError;
819 ///
820 /// let available = vec!["simulate".to_string(), "validate".to_string()];
821 /// let error = ParseError::unknown_command_with_suggestions("simulat", &available);
822 /// match error {
823 /// ParseError::UnknownCommand { suggestions, .. } => {
824 /// assert!(suggestions.contains(&"simulate".to_string()));
825 /// }
826 /// _ => panic!("wrong variant"),
827 /// }
828 /// ```
829 pub fn unknown_command_with_suggestions(command: &str, available: &[String]) -> Self {
830 let suggestions = crate::error::find_similar_strings(command, available, 3);
831 Self::UnknownCommand {
832 command: command.to_string(),
833 suggestions,
834 }
835 }
836
837 /// Create an unknown option error with Levenshtein suggestions
838 ///
839 /// # Example
840 ///
841 /// ```
842 /// use dynamic_cli::error::ParseError;
843 ///
844 /// let available = vec!["--verbose".to_string(), "--output".to_string()];
845 /// let error = ParseError::unknown_option_with_suggestions("--verbos", "run", &available);
846 /// match error {
847 /// ParseError::UnknownOption { suggestions, .. } => {
848 /// assert!(suggestions.contains(&"--verbose".to_string()));
849 /// }
850 /// _ => panic!("wrong variant"),
851 /// }
852 /// ```
853 pub fn unknown_option_with_suggestions(
854 flag: &str,
855 command: &str,
856 available: &[String],
857 ) -> Self {
858 let suggestions = crate::error::find_similar_strings(flag, available, 2);
859 Self::UnknownOption {
860 flag: flag.to_string(),
861 command: command.to_string(),
862 suggestions,
863 }
864 }
865
866 /// Create a missing argument error with a help hint
867 ///
868 /// The suggestion automatically refers the user to `--help <command>`.
869 ///
870 /// # Example
871 ///
872 /// ```
873 /// use dynamic_cli::error::ParseError;
874 ///
875 /// let error = ParseError::missing_argument("filename", "process");
876 /// match error {
877 /// ParseError::MissingArgument { suggestion, .. } => {
878 /// assert!(suggestion.is_some());
879 /// }
880 /// _ => panic!("wrong variant"),
881 /// }
882 /// ```
883 pub fn missing_argument(argument: &str, command: &str) -> Self {
884 Self::MissingArgument {
885 argument: argument.to_string(),
886 command: command.to_string(),
887 suggestion: Some(format!("Run --help {command} to see required arguments.")),
888 }
889 }
890
891 /// Create a missing option error with a help hint
892 ///
893 /// The suggestion automatically refers the user to `--help <command>`.
894 ///
895 /// # Example
896 ///
897 /// ```
898 /// use dynamic_cli::error::ParseError;
899 ///
900 /// let error = ParseError::missing_option("output", "export");
901 /// match error {
902 /// ParseError::MissingOption { suggestion, .. } => {
903 /// assert!(suggestion.is_some());
904 /// }
905 /// _ => panic!("wrong variant"),
906 /// }
907 /// ```
908 pub fn missing_option(option: &str, command: &str) -> Self {
909 Self::MissingOption {
910 option: option.to_string(),
911 command: command.to_string(),
912 suggestion: Some(format!("Run --help {command} to see required options.")),
913 }
914 }
915
916 /// Create a too-many-arguments error with a help hint
917 ///
918 /// # Example
919 ///
920 /// ```
921 /// use dynamic_cli::error::ParseError;
922 ///
923 /// let error = ParseError::too_many_arguments("run", 1, 3);
924 /// match error {
925 /// ParseError::TooManyArguments { suggestion, .. } => {
926 /// assert!(suggestion.is_some());
927 /// }
928 /// _ => panic!("wrong variant"),
929 /// }
930 /// ```
931 pub fn too_many_arguments(command: &str, expected: usize, got: usize) -> Self {
932 Self::TooManyArguments {
933 command: command.to_string(),
934 expected,
935 got,
936 suggestion: Some(format!("Run --help {command} for the expected usage.")),
937 }
938 }
939}
940
941impl ConfigError {
942 /// Create a file-not-found error with a standard suggestion
943 ///
944 /// # Example
945 ///
946 /// ```
947 /// use dynamic_cli::error::ConfigError;
948 /// use std::path::PathBuf;
949 ///
950 /// let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
951 /// match error {
952 /// ConfigError::FileNotFound { suggestion, .. } => {
953 /// assert!(suggestion.is_some());
954 /// }
955 /// _ => panic!("wrong variant"),
956 /// }
957 /// ```
958 pub fn file_not_found(path: PathBuf) -> Self {
959 Self::FileNotFound {
960 path,
961 suggestion: Some("Verify the path and file permissions.".to_string()),
962 }
963 }
964
965 /// Create an unsupported-format error with a standard suggestion
966 ///
967 /// # Example
968 ///
969 /// ```
970 /// use dynamic_cli::error::ConfigError;
971 ///
972 /// let error = ConfigError::unsupported_format(".toml");
973 /// match error {
974 /// ConfigError::UnsupportedFormat { suggestion, .. } => {
975 /// assert!(suggestion.is_some());
976 /// }
977 /// _ => panic!("wrong variant"),
978 /// }
979 /// ```
980 pub fn unsupported_format(extension: &str) -> Self {
981 Self::UnsupportedFormat {
982 extension: extension.to_string(),
983 suggestion: Some("Rename the file with a .yaml, .yml or .json extension.".to_string()),
984 }
985 }
986
987 /// Create a YAML parse error with position extracted from the serde error
988 pub fn yaml_parse_with_location(source: serde_yaml::Error) -> Self {
989 let location = source.location();
990 Self::YamlParse {
991 source,
992 line: location.as_ref().map(|l| l.line()),
993 column: location.map(|l| l.column()),
994 }
995 }
996
997 /// Create a JSON parse error with position extracted from the serde error
998 pub fn json_parse_with_location(source: serde_json::Error) -> Self {
999 Self::JsonParse {
1000 line: source.line(),
1001 column: source.column(),
1002 source,
1003 }
1004 }
1005}
1006
1007impl ExecutionError {
1008 /// Create a handler-not-found error with an actionable suggestion
1009 ///
1010 /// The suggestion interpolates the implementation name so the user
1011 /// knows exactly which `register_sync_handler()` or
1012 /// `register_async_handler()` call is missing (DD-022).
1013 ///
1014 /// # Example
1015 ///
1016 /// ```
1017 /// use dynamic_cli::error::ExecutionError;
1018 ///
1019 /// let error = ExecutionError::handler_not_found("run", "run_handler");
1020 /// match error {
1021 /// ExecutionError::HandlerNotFound { suggestion, .. } => {
1022 /// assert!(suggestion.as_deref().unwrap_or("").contains("run_handler"));
1023 /// }
1024 /// _ => panic!("wrong variant"),
1025 /// }
1026 /// ```
1027 pub fn handler_not_found(command: &str, implementation: &str) -> Self {
1028 Self::HandlerNotFound {
1029 command: command.to_string(),
1030 implementation: implementation.to_string(),
1031 suggestion: Some(format!(
1032 "Ensure .register_sync_handler(\"{implementation}\", ...) or \
1033 .register_async_handler(\"{implementation}\", ...) was called before running."
1034 )),
1035 }
1036 }
1037}
1038
1039impl RegistryError {
1040 /// Create a missing-handler error with an actionable suggestion
1041 ///
1042 /// The suggestion interpolates the command name so the user
1043 /// knows exactly which `.register_sync_handler()` call is missing.
1044 ///
1045 /// # Example
1046 ///
1047 /// ```
1048 /// use dynamic_cli::error::RegistryError;
1049 ///
1050 /// let error = RegistryError::missing_handler("export");
1051 /// match error {
1052 /// RegistryError::MissingHandler { suggestion, .. } => {
1053 /// assert!(suggestion.as_deref().unwrap_or("").contains("export"));
1054 /// }
1055 /// _ => panic!("wrong variant"),
1056 /// }
1057 /// ```
1058 pub fn missing_handler(command: &str) -> Self {
1059 Self::MissingHandler {
1060 command: command.to_string(),
1061 suggestion: Some(format!(
1062 "Call .register_sync_handler(\"{command}\", ...) before running."
1063 )),
1064 }
1065 }
1066}
1067
1068#[cfg(feature = "wasm-plugins")]
1069impl WasmError {
1070 /// Create a `FunctionNotFound` error for a missing mandatory export
1071 ///
1072 /// The suggestion names the exact signature expected for the missing
1073 /// export, taken from `WASM_PLUGIN_INTERFACE.md`.
1074 ///
1075 /// # Example
1076 ///
1077 /// ```
1078 /// use dynamic_cli::error::WasmError;
1079 ///
1080 /// let error = WasmError::missing_mandatory_export("dcli_alloc", "plugin.wasm");
1081 /// match error {
1082 /// WasmError::FunctionNotFound { suggestion, .. } => {
1083 /// assert!(suggestion.is_some());
1084 /// }
1085 /// _ => panic!("wrong variant"),
1086 /// }
1087 /// ```
1088 pub fn missing_mandatory_export(function: &str, module: &str) -> Self {
1089 let signature = match function {
1090 "dcli_alloc" => "fn dcli_alloc(size: i32) -> i32",
1091 "dcli_dealloc" => "fn dcli_dealloc(ptr: i32, size: i32)",
1092 "memory" => "(memory (export \"memory\") ...)",
1093 other => other,
1094 };
1095 Self::FunctionNotFound {
1096 function: function.to_string(),
1097 module: module.to_string(),
1098 suggestion: Some(format!(
1099 "Export `{signature}` from the WASM module. \
1100 See WASM_PLUGIN_INTERFACE.md for the full contract."
1101 )),
1102 }
1103 }
1104
1105 /// Create a `GuestError` from a raw error code, without a detailed message
1106 ///
1107 /// Used when the module does not export `dcli_last_error_message`.
1108 ///
1109 /// # Example
1110 ///
1111 /// ```
1112 /// use dynamic_cli::error::WasmError;
1113 ///
1114 /// let error = WasmError::guest_error_without_message(2);
1115 /// match error {
1116 /// WasmError::GuestError { code, message } => {
1117 /// assert_eq!(code, 2);
1118 /// assert!(message.is_none());
1119 /// }
1120 /// _ => panic!("wrong variant"),
1121 /// }
1122 /// ```
1123 pub fn guest_error_without_message(code: i32) -> Self {
1124 Self::GuestError {
1125 code,
1126 message: None,
1127 }
1128 }
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133 use super::*;
1134
1135 // ── ConfigError ──────────────────────────────────────────
1136
1137 #[test]
1138 fn test_config_file_not_found_display() {
1139 let error = ConfigError::FileNotFound {
1140 path: PathBuf::from("/path/to/config.yaml"),
1141 suggestion: None,
1142 };
1143 let msg = format!("{}", error);
1144 assert!(msg.contains("not found"));
1145 assert!(msg.contains("config.yaml"));
1146 }
1147
1148 #[test]
1149 fn test_config_file_not_found_helper_has_suggestion() {
1150 let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
1151 match error {
1152 ConfigError::FileNotFound { suggestion, .. } => {
1153 assert!(suggestion.is_some(), "helper must populate suggestion");
1154 }
1155 _ => panic!("wrong variant"),
1156 }
1157 }
1158
1159 #[test]
1160 fn test_config_unsupported_format_helper_has_suggestion() {
1161 let error = ConfigError::unsupported_format(".toml");
1162 match error {
1163 ConfigError::UnsupportedFormat {
1164 suggestion,
1165 extension,
1166 ..
1167 } => {
1168 assert_eq!(extension, ".toml");
1169 assert!(suggestion.is_some());
1170 }
1171 _ => panic!("wrong variant"),
1172 }
1173 }
1174
1175 #[test]
1176 fn test_config_duplicate_command_display() {
1177 let error = ConfigError::DuplicateCommand {
1178 name: "run".to_string(),
1179 suggestion: Some("Rename one of the conflicting commands.".to_string()),
1180 };
1181 let msg = format!("{}", error);
1182 assert!(msg.contains("run"));
1183 // suggestion must NOT appear in Display (it's rendered separately)
1184 assert!(!msg.contains("Rename"));
1185 }
1186
1187 #[test]
1188 fn test_config_unknown_type_display() {
1189 let error = ConfigError::UnknownType {
1190 type_name: "datetime".to_string(),
1191 context: "commands[0]".to_string(),
1192 suggestion: None,
1193 };
1194 let msg = format!("{}", error);
1195 assert!(msg.contains("datetime"));
1196 }
1197
1198 #[test]
1199 fn test_config_inconsistency_display() {
1200 let error = ConfigError::Inconsistency {
1201 details: "default not in choices".to_string(),
1202 suggestion: Some("hint".to_string()),
1203 };
1204 let msg = format!("{}", error);
1205 assert!(msg.contains("default not in choices"));
1206 assert!(!msg.contains("hint")); // suggestion separate from Display
1207 }
1208
1209 #[test]
1210 fn test_config_invalid_schema_display() {
1211 let error = ConfigError::InvalidSchema {
1212 reason: "missing field".to_string(),
1213 path: Some("commands[0]".to_string()),
1214 suggestion: None,
1215 };
1216 let msg = format!("{}", error);
1217 assert!(msg.contains("missing field"));
1218 }
1219
1220 // ── ParseError ───────────────────────────────────────────
1221
1222 #[test]
1223 fn test_parse_unknown_command_with_suggestions() {
1224 let available = vec!["simulate".to_string(), "validate".to_string()];
1225 let error = ParseError::unknown_command_with_suggestions("simulat", &available);
1226 match error {
1227 ParseError::UnknownCommand {
1228 command,
1229 suggestions,
1230 } => {
1231 assert_eq!(command, "simulat");
1232 assert!(suggestions.contains(&"simulate".to_string()));
1233 }
1234 _ => panic!("wrong variant"),
1235 }
1236 }
1237
1238 #[test]
1239 fn test_parse_missing_argument_helper_has_suggestion() {
1240 let error = ParseError::missing_argument("filename", "process");
1241 match error {
1242 ParseError::MissingArgument {
1243 suggestion,
1244 command,
1245 ..
1246 } => {
1247 assert_eq!(command, "process");
1248 let s = suggestion.unwrap();
1249 assert!(s.contains("process"));
1250 assert!(s.contains("--help"));
1251 }
1252 _ => panic!("wrong variant"),
1253 }
1254 }
1255
1256 #[test]
1257 fn test_parse_missing_option_helper_has_suggestion() {
1258 let error = ParseError::missing_option("output", "export");
1259 match error {
1260 ParseError::MissingOption {
1261 suggestion, option, ..
1262 } => {
1263 assert_eq!(option, "output");
1264 let s = suggestion.unwrap();
1265 assert!(s.contains("export"));
1266 }
1267 _ => panic!("wrong variant"),
1268 }
1269 }
1270
1271 #[test]
1272 fn test_parse_too_many_arguments_helper_has_suggestion() {
1273 let error = ParseError::too_many_arguments("run", 1, 3);
1274 match error {
1275 ParseError::TooManyArguments {
1276 suggestion,
1277 expected,
1278 got,
1279 ..
1280 } => {
1281 assert_eq!(expected, 1);
1282 assert_eq!(got, 3);
1283 assert!(suggestion.is_some());
1284 }
1285 _ => panic!("wrong variant"),
1286 }
1287 }
1288
1289 #[test]
1290 fn test_parse_missing_argument_suggestion_none_by_default() {
1291 // Direct construction without helper: suggestion is caller's responsibility
1292 let error = ParseError::MissingArgument {
1293 argument: "file".to_string(),
1294 command: "run".to_string(),
1295 suggestion: None,
1296 };
1297 match error {
1298 ParseError::MissingArgument { suggestion, .. } => assert!(suggestion.is_none()),
1299 _ => panic!("wrong variant"),
1300 }
1301 }
1302
1303 // ── ValidationError ──────────────────────────────────────
1304
1305 #[test]
1306 fn test_validation_out_of_range_display() {
1307 let error = ValidationError::OutOfRange {
1308 arg_name: "percentage".to_string(),
1309 value: 150.0,
1310 min: 0.0,
1311 max: 100.0,
1312 suggestion: None,
1313 };
1314 let msg = format!("{}", error);
1315 assert!(msg.contains("percentage"));
1316 assert!(msg.contains("150"));
1317 assert!(msg.contains("0"));
1318 assert!(msg.contains("100"));
1319 }
1320
1321 #[test]
1322 fn test_validation_out_of_range_suggestion_not_in_display() {
1323 let error = ValidationError::OutOfRange {
1324 arg_name: "percentage".to_string(),
1325 value: 150.0,
1326 min: 0.0,
1327 max: 100.0,
1328 suggestion: Some("Value must be between 0 and 100.".to_string()),
1329 };
1330 let msg = format!("{}", error);
1331 assert!(!msg.contains("Value must be between")); // suggestion is separate
1332 }
1333
1334 #[test]
1335 fn test_validation_file_not_found_suggestion() {
1336 let error = ValidationError::FileNotFound {
1337 path: PathBuf::from("data.csv"),
1338 arg_name: "input".to_string(),
1339 suggestion: Some("Check that the file exists.".to_string()),
1340 };
1341 match error {
1342 ValidationError::FileNotFound { suggestion, .. } => {
1343 assert!(suggestion.is_some());
1344 }
1345 _ => panic!("wrong variant"),
1346 }
1347 }
1348
1349 #[test]
1350 fn test_validation_missing_dependency_suggestion() {
1351 let error = ValidationError::MissingDependency {
1352 arg_name: "format".to_string(),
1353 required_arg: "output".to_string(),
1354 suggestion: Some("Add --output to your command.".to_string()),
1355 };
1356 let msg = format!("{}", error);
1357 assert!(msg.contains("format"));
1358 assert!(msg.contains("output"));
1359 }
1360
1361 #[test]
1362 fn test_validation_mutually_exclusive_suggestion() {
1363 let error = ValidationError::MutuallyExclusive {
1364 arg1: "--verbose".to_string(),
1365 arg2: "--quiet".to_string(),
1366 suggestion: Some("Remove one of the two conflicting options.".to_string()),
1367 };
1368 let msg = format!("{}", error);
1369 assert!(msg.contains("--verbose"));
1370 assert!(msg.contains("--quiet"));
1371 }
1372
1373 // ── ExecutionError ───────────────────────────────────────
1374
1375 #[test]
1376 fn test_execution_handler_not_found_helper_interpolates_impl() {
1377 let error = ExecutionError::handler_not_found("run", "run_handler");
1378 match error {
1379 ExecutionError::HandlerNotFound {
1380 suggestion,
1381 implementation,
1382 ..
1383 } => {
1384 assert_eq!(implementation, "run_handler");
1385 let s = suggestion.unwrap();
1386 assert!(s.contains("run_handler"));
1387 assert!(s.contains("register_sync_handler"));
1388 }
1389 _ => panic!("wrong variant"),
1390 }
1391 }
1392
1393 #[test]
1394 fn test_execution_context_downcast_failed_display() {
1395 let error = ExecutionError::ContextDowncastFailed {
1396 expected_type: "MyAppContext".to_string(),
1397 suggestion: None,
1398 };
1399 let msg = format!("{}", error);
1400 assert!(msg.contains("MyAppContext"));
1401 }
1402
1403 #[test]
1404 fn test_execution_invalid_context_state_suggestion() {
1405 let error = ExecutionError::InvalidContextState {
1406 reason: "pool not ready".to_string(),
1407 suggestion: Some("Ensure context is initialised.".to_string()),
1408 };
1409 let msg = format!("{}", error);
1410 assert!(msg.contains("pool not ready"));
1411 }
1412
1413 // ── RegistryError ────────────────────────────────────────
1414
1415 #[test]
1416 fn test_registry_missing_handler_helper_interpolates_command() {
1417 let error = RegistryError::missing_handler("export");
1418 match error {
1419 RegistryError::MissingHandler {
1420 suggestion,
1421 command,
1422 } => {
1423 assert_eq!(command, "export");
1424 let s = suggestion.unwrap();
1425 assert!(s.contains("export"));
1426 assert!(s.contains("register_sync_handler"));
1427 }
1428 _ => panic!("wrong variant"),
1429 }
1430 }
1431
1432 #[test]
1433 fn test_registry_duplicate_registration_display() {
1434 let error = RegistryError::DuplicateRegistration {
1435 name: "run".to_string(),
1436 suggestion: None,
1437 };
1438 let msg = format!("{}", error);
1439 assert!(msg.contains("run"));
1440 }
1441
1442 #[test]
1443 fn test_registry_duplicate_alias_display() {
1444 let error = RegistryError::DuplicateAlias {
1445 alias: "r".to_string(),
1446 existing_command: "run".to_string(),
1447 suggestion: Some("Choose a different alias.".to_string()),
1448 };
1449 let msg = format!("{}", error);
1450 assert!(msg.contains("run"));
1451 assert!(!msg.contains("Choose")); // suggestion separate from Display
1452 }
1453
1454 // ── WasmError ────────────────────────────────────────────
1455
1456 #[cfg(feature = "wasm-plugins")]
1457 #[test]
1458 fn test_wasm_load_failed_display() {
1459 let error = WasmError::LoadFailed {
1460 path: PathBuf::from("plugin.wasm"),
1461 source: anyhow::anyhow!("invalid magic number"),
1462 suggestion: None,
1463 };
1464 let msg = format!("{}", error);
1465 assert!(msg.contains("plugin.wasm"));
1466 assert!(msg.contains("invalid magic number"));
1467 }
1468
1469 #[cfg(feature = "wasm-plugins")]
1470 #[test]
1471 fn test_wasm_function_not_found_display() {
1472 let error = WasmError::FunctionNotFound {
1473 function: "dcli_alloc".to_string(),
1474 module: "plugin.wasm".to_string(),
1475 suggestion: None,
1476 };
1477 let msg = format!("{}", error);
1478 assert!(msg.contains("dcli_alloc"));
1479 assert!(msg.contains("plugin.wasm"));
1480 }
1481
1482 #[cfg(feature = "wasm-plugins")]
1483 #[test]
1484 fn test_wasm_missing_mandatory_export_helper_has_suggestion() {
1485 let error = WasmError::missing_mandatory_export("dcli_dealloc", "plugin.wasm");
1486 match error {
1487 WasmError::FunctionNotFound {
1488 suggestion,
1489 function,
1490 ..
1491 } => {
1492 assert_eq!(function, "dcli_dealloc");
1493 let s = suggestion.unwrap();
1494 assert!(s.contains("dcli_dealloc"));
1495 assert!(s.contains("WASM_PLUGIN_INTERFACE.md"));
1496 }
1497 _ => panic!("wrong variant"),
1498 }
1499 }
1500
1501 #[cfg(feature = "wasm-plugins")]
1502 #[test]
1503 fn test_wasm_guest_error_with_message_display() {
1504 let error = WasmError::GuestError {
1505 code: 1,
1506 message: Some("invalid argument".to_string()),
1507 };
1508 let msg = format!("{}", error);
1509 assert!(msg.contains('1'));
1510 assert!(msg.contains("invalid argument"));
1511 }
1512
1513 #[cfg(feature = "wasm-plugins")]
1514 #[test]
1515 fn test_wasm_guest_error_without_message_display() {
1516 let error = WasmError::guest_error_without_message(2);
1517 let msg = format!("{}", error);
1518 assert!(msg.contains('2'));
1519 match error {
1520 WasmError::GuestError { message, .. } => assert!(message.is_none()),
1521 _ => panic!("wrong variant"),
1522 }
1523 }
1524
1525 #[cfg(feature = "wasm-plugins")]
1526 #[test]
1527 fn test_wasm_serialization_failed_display() {
1528 let error = WasmError::SerializationFailed("unsupported map key type".to_string());
1529 let msg = format!("{}", error);
1530 assert!(msg.contains("unsupported map key type"));
1531 }
1532
1533 #[cfg(feature = "wasm-plugins")]
1534 #[test]
1535 fn test_wasm_memory_access_failed_display() {
1536 let error = WasmError::MemoryAccessFailed {
1537 reason: "write out of bounds".to_string(),
1538 };
1539 let msg = format!("{}", error);
1540 assert!(msg.contains("out of bounds"));
1541 }
1542
1543 #[cfg(feature = "wasm-plugins")]
1544 #[test]
1545 fn test_wasm_error_converts_into_dynamic_cli_error() {
1546 let wasm_err = WasmError::guest_error_without_message(1);
1547 let err: DynamicCliError = wasm_err.into();
1548 match err {
1549 DynamicCliError::Wasm(WasmError::GuestError { code, .. }) => assert_eq!(code, 1),
1550 _ => panic!("wrong variant"),
1551 }
1552 }
1553}