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_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_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_handler()` call is missing.
1012 ///
1013 /// # Example
1014 ///
1015 /// ```
1016 /// use dynamic_cli::error::ExecutionError;
1017 ///
1018 /// let error = ExecutionError::handler_not_found("run", "run_handler");
1019 /// match error {
1020 /// ExecutionError::HandlerNotFound { suggestion, .. } => {
1021 /// assert!(suggestion.as_deref().unwrap_or("").contains("run_handler"));
1022 /// }
1023 /// _ => panic!("wrong variant"),
1024 /// }
1025 /// ```
1026 pub fn handler_not_found(command: &str, implementation: &str) -> Self {
1027 Self::HandlerNotFound {
1028 command: command.to_string(),
1029 implementation: implementation.to_string(),
1030 suggestion: Some(format!(
1031 "Ensure .register_handler(\"{implementation}\", ...) was called before running."
1032 )),
1033 }
1034 }
1035}
1036
1037impl RegistryError {
1038 /// Create a missing-handler error with an actionable suggestion
1039 ///
1040 /// The suggestion interpolates the command name so the user
1041 /// knows exactly which `.register_handler()` call is missing.
1042 ///
1043 /// # Example
1044 ///
1045 /// ```
1046 /// use dynamic_cli::error::RegistryError;
1047 ///
1048 /// let error = RegistryError::missing_handler("export");
1049 /// match error {
1050 /// RegistryError::MissingHandler { suggestion, .. } => {
1051 /// assert!(suggestion.as_deref().unwrap_or("").contains("export"));
1052 /// }
1053 /// _ => panic!("wrong variant"),
1054 /// }
1055 /// ```
1056 pub fn missing_handler(command: &str) -> Self {
1057 Self::MissingHandler {
1058 command: command.to_string(),
1059 suggestion: Some(format!(
1060 "Call .register_handler(\"{command}\", ...) before running."
1061 )),
1062 }
1063 }
1064}
1065
1066#[cfg(feature = "wasm-plugins")]
1067impl WasmError {
1068 /// Create a `FunctionNotFound` error for a missing mandatory export
1069 ///
1070 /// The suggestion names the exact signature expected for the missing
1071 /// export, taken from `WASM_PLUGIN_INTERFACE.md`.
1072 ///
1073 /// # Example
1074 ///
1075 /// ```
1076 /// use dynamic_cli::error::WasmError;
1077 ///
1078 /// let error = WasmError::missing_mandatory_export("dcli_alloc", "plugin.wasm");
1079 /// match error {
1080 /// WasmError::FunctionNotFound { suggestion, .. } => {
1081 /// assert!(suggestion.is_some());
1082 /// }
1083 /// _ => panic!("wrong variant"),
1084 /// }
1085 /// ```
1086 pub fn missing_mandatory_export(function: &str, module: &str) -> Self {
1087 let signature = match function {
1088 "dcli_alloc" => "fn dcli_alloc(size: i32) -> i32",
1089 "dcli_dealloc" => "fn dcli_dealloc(ptr: i32, size: i32)",
1090 "memory" => "(memory (export \"memory\") ...)",
1091 other => other,
1092 };
1093 Self::FunctionNotFound {
1094 function: function.to_string(),
1095 module: module.to_string(),
1096 suggestion: Some(format!(
1097 "Export `{signature}` from the WASM module. \
1098 See WASM_PLUGIN_INTERFACE.md for the full contract."
1099 )),
1100 }
1101 }
1102
1103 /// Create a `GuestError` from a raw error code, without a detailed message
1104 ///
1105 /// Used when the module does not export `dcli_last_error_message`.
1106 ///
1107 /// # Example
1108 ///
1109 /// ```
1110 /// use dynamic_cli::error::WasmError;
1111 ///
1112 /// let error = WasmError::guest_error_without_message(2);
1113 /// match error {
1114 /// WasmError::GuestError { code, message } => {
1115 /// assert_eq!(code, 2);
1116 /// assert!(message.is_none());
1117 /// }
1118 /// _ => panic!("wrong variant"),
1119 /// }
1120 /// ```
1121 pub fn guest_error_without_message(code: i32) -> Self {
1122 Self::GuestError {
1123 code,
1124 message: None,
1125 }
1126 }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132
1133 // ── ConfigError ──────────────────────────────────────────
1134
1135 #[test]
1136 fn test_config_file_not_found_display() {
1137 let error = ConfigError::FileNotFound {
1138 path: PathBuf::from("/path/to/config.yaml"),
1139 suggestion: None,
1140 };
1141 let msg = format!("{}", error);
1142 assert!(msg.contains("not found"));
1143 assert!(msg.contains("config.yaml"));
1144 }
1145
1146 #[test]
1147 fn test_config_file_not_found_helper_has_suggestion() {
1148 let error = ConfigError::file_not_found(PathBuf::from("commands.yaml"));
1149 match error {
1150 ConfigError::FileNotFound { suggestion, .. } => {
1151 assert!(suggestion.is_some(), "helper must populate suggestion");
1152 }
1153 _ => panic!("wrong variant"),
1154 }
1155 }
1156
1157 #[test]
1158 fn test_config_unsupported_format_helper_has_suggestion() {
1159 let error = ConfigError::unsupported_format(".toml");
1160 match error {
1161 ConfigError::UnsupportedFormat {
1162 suggestion,
1163 extension,
1164 ..
1165 } => {
1166 assert_eq!(extension, ".toml");
1167 assert!(suggestion.is_some());
1168 }
1169 _ => panic!("wrong variant"),
1170 }
1171 }
1172
1173 #[test]
1174 fn test_config_duplicate_command_display() {
1175 let error = ConfigError::DuplicateCommand {
1176 name: "run".to_string(),
1177 suggestion: Some("Rename one of the conflicting commands.".to_string()),
1178 };
1179 let msg = format!("{}", error);
1180 assert!(msg.contains("run"));
1181 // suggestion must NOT appear in Display (it's rendered separately)
1182 assert!(!msg.contains("Rename"));
1183 }
1184
1185 #[test]
1186 fn test_config_unknown_type_display() {
1187 let error = ConfigError::UnknownType {
1188 type_name: "datetime".to_string(),
1189 context: "commands[0]".to_string(),
1190 suggestion: None,
1191 };
1192 let msg = format!("{}", error);
1193 assert!(msg.contains("datetime"));
1194 }
1195
1196 #[test]
1197 fn test_config_inconsistency_display() {
1198 let error = ConfigError::Inconsistency {
1199 details: "default not in choices".to_string(),
1200 suggestion: Some("hint".to_string()),
1201 };
1202 let msg = format!("{}", error);
1203 assert!(msg.contains("default not in choices"));
1204 assert!(!msg.contains("hint")); // suggestion separate from Display
1205 }
1206
1207 #[test]
1208 fn test_config_invalid_schema_display() {
1209 let error = ConfigError::InvalidSchema {
1210 reason: "missing field".to_string(),
1211 path: Some("commands[0]".to_string()),
1212 suggestion: None,
1213 };
1214 let msg = format!("{}", error);
1215 assert!(msg.contains("missing field"));
1216 }
1217
1218 // ── ParseError ───────────────────────────────────────────
1219
1220 #[test]
1221 fn test_parse_unknown_command_with_suggestions() {
1222 let available = vec!["simulate".to_string(), "validate".to_string()];
1223 let error = ParseError::unknown_command_with_suggestions("simulat", &available);
1224 match error {
1225 ParseError::UnknownCommand {
1226 command,
1227 suggestions,
1228 } => {
1229 assert_eq!(command, "simulat");
1230 assert!(suggestions.contains(&"simulate".to_string()));
1231 }
1232 _ => panic!("wrong variant"),
1233 }
1234 }
1235
1236 #[test]
1237 fn test_parse_missing_argument_helper_has_suggestion() {
1238 let error = ParseError::missing_argument("filename", "process");
1239 match error {
1240 ParseError::MissingArgument {
1241 suggestion,
1242 command,
1243 ..
1244 } => {
1245 assert_eq!(command, "process");
1246 let s = suggestion.unwrap();
1247 assert!(s.contains("process"));
1248 assert!(s.contains("--help"));
1249 }
1250 _ => panic!("wrong variant"),
1251 }
1252 }
1253
1254 #[test]
1255 fn test_parse_missing_option_helper_has_suggestion() {
1256 let error = ParseError::missing_option("output", "export");
1257 match error {
1258 ParseError::MissingOption {
1259 suggestion, option, ..
1260 } => {
1261 assert_eq!(option, "output");
1262 let s = suggestion.unwrap();
1263 assert!(s.contains("export"));
1264 }
1265 _ => panic!("wrong variant"),
1266 }
1267 }
1268
1269 #[test]
1270 fn test_parse_too_many_arguments_helper_has_suggestion() {
1271 let error = ParseError::too_many_arguments("run", 1, 3);
1272 match error {
1273 ParseError::TooManyArguments {
1274 suggestion,
1275 expected,
1276 got,
1277 ..
1278 } => {
1279 assert_eq!(expected, 1);
1280 assert_eq!(got, 3);
1281 assert!(suggestion.is_some());
1282 }
1283 _ => panic!("wrong variant"),
1284 }
1285 }
1286
1287 #[test]
1288 fn test_parse_missing_argument_suggestion_none_by_default() {
1289 // Direct construction without helper: suggestion is caller's responsibility
1290 let error = ParseError::MissingArgument {
1291 argument: "file".to_string(),
1292 command: "run".to_string(),
1293 suggestion: None,
1294 };
1295 match error {
1296 ParseError::MissingArgument { suggestion, .. } => assert!(suggestion.is_none()),
1297 _ => panic!("wrong variant"),
1298 }
1299 }
1300
1301 // ── ValidationError ──────────────────────────────────────
1302
1303 #[test]
1304 fn test_validation_out_of_range_display() {
1305 let error = ValidationError::OutOfRange {
1306 arg_name: "percentage".to_string(),
1307 value: 150.0,
1308 min: 0.0,
1309 max: 100.0,
1310 suggestion: None,
1311 };
1312 let msg = format!("{}", error);
1313 assert!(msg.contains("percentage"));
1314 assert!(msg.contains("150"));
1315 assert!(msg.contains("0"));
1316 assert!(msg.contains("100"));
1317 }
1318
1319 #[test]
1320 fn test_validation_out_of_range_suggestion_not_in_display() {
1321 let error = ValidationError::OutOfRange {
1322 arg_name: "percentage".to_string(),
1323 value: 150.0,
1324 min: 0.0,
1325 max: 100.0,
1326 suggestion: Some("Value must be between 0 and 100.".to_string()),
1327 };
1328 let msg = format!("{}", error);
1329 assert!(!msg.contains("Value must be between")); // suggestion is separate
1330 }
1331
1332 #[test]
1333 fn test_validation_file_not_found_suggestion() {
1334 let error = ValidationError::FileNotFound {
1335 path: PathBuf::from("data.csv"),
1336 arg_name: "input".to_string(),
1337 suggestion: Some("Check that the file exists.".to_string()),
1338 };
1339 match error {
1340 ValidationError::FileNotFound { suggestion, .. } => {
1341 assert!(suggestion.is_some());
1342 }
1343 _ => panic!("wrong variant"),
1344 }
1345 }
1346
1347 #[test]
1348 fn test_validation_missing_dependency_suggestion() {
1349 let error = ValidationError::MissingDependency {
1350 arg_name: "format".to_string(),
1351 required_arg: "output".to_string(),
1352 suggestion: Some("Add --output to your command.".to_string()),
1353 };
1354 let msg = format!("{}", error);
1355 assert!(msg.contains("format"));
1356 assert!(msg.contains("output"));
1357 }
1358
1359 #[test]
1360 fn test_validation_mutually_exclusive_suggestion() {
1361 let error = ValidationError::MutuallyExclusive {
1362 arg1: "--verbose".to_string(),
1363 arg2: "--quiet".to_string(),
1364 suggestion: Some("Remove one of the two conflicting options.".to_string()),
1365 };
1366 let msg = format!("{}", error);
1367 assert!(msg.contains("--verbose"));
1368 assert!(msg.contains("--quiet"));
1369 }
1370
1371 // ── ExecutionError ───────────────────────────────────────
1372
1373 #[test]
1374 fn test_execution_handler_not_found_helper_interpolates_impl() {
1375 let error = ExecutionError::handler_not_found("run", "run_handler");
1376 match error {
1377 ExecutionError::HandlerNotFound {
1378 suggestion,
1379 implementation,
1380 ..
1381 } => {
1382 assert_eq!(implementation, "run_handler");
1383 let s = suggestion.unwrap();
1384 assert!(s.contains("run_handler"));
1385 assert!(s.contains("register_handler"));
1386 }
1387 _ => panic!("wrong variant"),
1388 }
1389 }
1390
1391 #[test]
1392 fn test_execution_context_downcast_failed_display() {
1393 let error = ExecutionError::ContextDowncastFailed {
1394 expected_type: "MyAppContext".to_string(),
1395 suggestion: None,
1396 };
1397 let msg = format!("{}", error);
1398 assert!(msg.contains("MyAppContext"));
1399 }
1400
1401 #[test]
1402 fn test_execution_invalid_context_state_suggestion() {
1403 let error = ExecutionError::InvalidContextState {
1404 reason: "pool not ready".to_string(),
1405 suggestion: Some("Ensure context is initialised.".to_string()),
1406 };
1407 let msg = format!("{}", error);
1408 assert!(msg.contains("pool not ready"));
1409 }
1410
1411 // ── RegistryError ────────────────────────────────────────
1412
1413 #[test]
1414 fn test_registry_missing_handler_helper_interpolates_command() {
1415 let error = RegistryError::missing_handler("export");
1416 match error {
1417 RegistryError::MissingHandler {
1418 suggestion,
1419 command,
1420 } => {
1421 assert_eq!(command, "export");
1422 let s = suggestion.unwrap();
1423 assert!(s.contains("export"));
1424 assert!(s.contains("register_handler"));
1425 }
1426 _ => panic!("wrong variant"),
1427 }
1428 }
1429
1430 #[test]
1431 fn test_registry_duplicate_registration_display() {
1432 let error = RegistryError::DuplicateRegistration {
1433 name: "run".to_string(),
1434 suggestion: None,
1435 };
1436 let msg = format!("{}", error);
1437 assert!(msg.contains("run"));
1438 }
1439
1440 #[test]
1441 fn test_registry_duplicate_alias_display() {
1442 let error = RegistryError::DuplicateAlias {
1443 alias: "r".to_string(),
1444 existing_command: "run".to_string(),
1445 suggestion: Some("Choose a different alias.".to_string()),
1446 };
1447 let msg = format!("{}", error);
1448 assert!(msg.contains("run"));
1449 assert!(!msg.contains("Choose")); // suggestion separate from Display
1450 }
1451
1452 // ── WasmError ────────────────────────────────────────────
1453
1454 #[cfg(feature = "wasm-plugins")]
1455 #[test]
1456 fn test_wasm_load_failed_display() {
1457 let error = WasmError::LoadFailed {
1458 path: PathBuf::from("plugin.wasm"),
1459 source: anyhow::anyhow!("invalid magic number"),
1460 suggestion: None,
1461 };
1462 let msg = format!("{}", error);
1463 assert!(msg.contains("plugin.wasm"));
1464 assert!(msg.contains("invalid magic number"));
1465 }
1466
1467 #[cfg(feature = "wasm-plugins")]
1468 #[test]
1469 fn test_wasm_function_not_found_display() {
1470 let error = WasmError::FunctionNotFound {
1471 function: "dcli_alloc".to_string(),
1472 module: "plugin.wasm".to_string(),
1473 suggestion: None,
1474 };
1475 let msg = format!("{}", error);
1476 assert!(msg.contains("dcli_alloc"));
1477 assert!(msg.contains("plugin.wasm"));
1478 }
1479
1480 #[cfg(feature = "wasm-plugins")]
1481 #[test]
1482 fn test_wasm_missing_mandatory_export_helper_has_suggestion() {
1483 let error = WasmError::missing_mandatory_export("dcli_dealloc", "plugin.wasm");
1484 match error {
1485 WasmError::FunctionNotFound {
1486 suggestion,
1487 function,
1488 ..
1489 } => {
1490 assert_eq!(function, "dcli_dealloc");
1491 let s = suggestion.unwrap();
1492 assert!(s.contains("dcli_dealloc"));
1493 assert!(s.contains("WASM_PLUGIN_INTERFACE.md"));
1494 }
1495 _ => panic!("wrong variant"),
1496 }
1497 }
1498
1499 #[cfg(feature = "wasm-plugins")]
1500 #[test]
1501 fn test_wasm_guest_error_with_message_display() {
1502 let error = WasmError::GuestError {
1503 code: 1,
1504 message: Some("invalid argument".to_string()),
1505 };
1506 let msg = format!("{}", error);
1507 assert!(msg.contains('1'));
1508 assert!(msg.contains("invalid argument"));
1509 }
1510
1511 #[cfg(feature = "wasm-plugins")]
1512 #[test]
1513 fn test_wasm_guest_error_without_message_display() {
1514 let error = WasmError::guest_error_without_message(2);
1515 let msg = format!("{}", error);
1516 assert!(msg.contains('2'));
1517 match error {
1518 WasmError::GuestError { message, .. } => assert!(message.is_none()),
1519 _ => panic!("wrong variant"),
1520 }
1521 }
1522
1523 #[cfg(feature = "wasm-plugins")]
1524 #[test]
1525 fn test_wasm_serialization_failed_display() {
1526 let error = WasmError::SerializationFailed("unsupported map key type".to_string());
1527 let msg = format!("{}", error);
1528 assert!(msg.contains("unsupported map key type"));
1529 }
1530
1531 #[cfg(feature = "wasm-plugins")]
1532 #[test]
1533 fn test_wasm_memory_access_failed_display() {
1534 let error = WasmError::MemoryAccessFailed {
1535 reason: "write out of bounds".to_string(),
1536 };
1537 let msg = format!("{}", error);
1538 assert!(msg.contains("out of bounds"));
1539 }
1540
1541 #[cfg(feature = "wasm-plugins")]
1542 #[test]
1543 fn test_wasm_error_converts_into_dynamic_cli_error() {
1544 let wasm_err = WasmError::guest_error_without_message(1);
1545 let err: DynamicCliError = wasm_err.into();
1546 match err {
1547 DynamicCliError::Wasm(WasmError::GuestError { code, .. }) => assert_eq!(code, 1),
1548 _ => panic!("wrong variant"),
1549 }
1550 }
1551}