Skip to main content

mabi_cli/
error.rs

1//! CLI error types and result aliases.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// CLI-specific error types.
7#[derive(Debug, Error)]
8pub enum CliError {
9    /// Configuration file not found.
10    #[error("Configuration file not found: {path}")]
11    ConfigNotFound { path: PathBuf },
12
13    /// Invalid configuration format.
14    #[error("Invalid configuration: {message}")]
15    InvalidConfig { message: String },
16
17    /// Scenario file not found.
18    #[error("Scenario file not found: {path}")]
19    ScenarioNotFound { path: PathBuf },
20
21    /// Invalid scenario format.
22    #[error("Invalid scenario: {message}")]
23    InvalidScenario { message: String },
24
25    /// Protocol not supported.
26    #[error("Protocol not supported: {protocol}")]
27    UnsupportedProtocol { protocol: String },
28
29    /// Device not found.
30    #[error("Device not found: {device_id}")]
31    DeviceNotFound { device_id: String },
32
33    /// Port already in use.
34    #[error("Port {port} is already in use")]
35    PortInUse { port: u16 },
36
37    /// Command execution failed.
38    #[error("Command execution failed: {message}")]
39    ExecutionFailed { message: String },
40
41    /// Validation failed.
42    #[error("Validation failed:\n{errors}")]
43    ValidationFailed { errors: String },
44
45    /// IO error.
46    #[error("IO error: {0}")]
47    Io(#[from] std::io::Error),
48
49    /// YAML parsing error.
50    #[error("YAML parsing error: {0}")]
51    Yaml(#[from] serde_yaml::Error),
52
53    /// JSON parsing error.
54    #[error("JSON parsing error: {0}")]
55    Json(#[from] serde_json::Error),
56
57    /// Core simulator error.
58    #[error("Simulator error: {0}")]
59    Simulator(#[from] mabi_core::Error),
60
61    /// User interrupted operation.
62    #[error("Operation interrupted by user")]
63    Interrupted,
64
65    /// Timeout reached.
66    #[error("Operation timed out after {duration_secs} seconds")]
67    Timeout { duration_secs: u64 },
68
69    /// Generic error with context.
70    #[error("{context}: {source}")]
71    WithContext {
72        context: String,
73        #[source]
74        source: Box<CliError>,
75    },
76}
77
78impl CliError {
79    /// Add context to an error.
80    pub fn with_context(self, context: impl Into<String>) -> Self {
81        CliError::WithContext {
82            context: context.into(),
83            source: Box::new(self),
84        }
85    }
86
87    /// Create a validation failed error from multiple messages.
88    pub fn validation_failed(errors: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
89        let errors: Vec<String> = errors.into_iter().map(|s| format!("  - {}", s.as_ref())).collect();
90        CliError::ValidationFailed {
91            errors: errors.join("\n"),
92        }
93    }
94
95    /// Get the exit code for this error.
96    pub fn exit_code(&self) -> i32 {
97        match self {
98            CliError::ConfigNotFound { .. } => 2,
99            CliError::InvalidConfig { .. } => 2,
100            CliError::ScenarioNotFound { .. } => 2,
101            CliError::InvalidScenario { .. } => 2,
102            CliError::UnsupportedProtocol { .. } => 3,
103            CliError::DeviceNotFound { .. } => 4,
104            CliError::PortInUse { .. } => 5,
105            CliError::ExecutionFailed { .. } => 1,
106            CliError::ValidationFailed { .. } => 6,
107            CliError::Io(_) => 7,
108            CliError::Yaml(_) | CliError::Json(_) => 8,
109            CliError::Simulator(_) => 9,
110            CliError::Interrupted => 130,
111            CliError::Timeout { .. } => 124,
112            CliError::WithContext { source, .. } => source.exit_code(),
113        }
114    }
115}
116
117/// CLI result type alias.
118pub type CliResult<T> = Result<T, CliError>;
119
120/// Extension trait for adding CLI context to results.
121pub trait CliResultExt<T> {
122    /// Add context to a result error.
123    fn cli_context(self, context: impl Into<String>) -> CliResult<T>;
124}
125
126impl<T, E: Into<CliError>> CliResultExt<T> for Result<T, E> {
127    fn cli_context(self, context: impl Into<String>) -> CliResult<T> {
128        self.map_err(|e| e.into().with_context(context))
129    }
130}