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             A previous mabi process may have been suspended (Ctrl+Z) and is still holding the port.\n  \
36             Diagnostic: lsof -i :{port} | grep LISTEN\n  \
37             To kill:    kill $(lsof -ti :{port} -sTCP:LISTEN)")]
38    PortInUse { port: u16 },
39
40    /// Command execution failed.
41    #[error("Command execution failed: {message}")]
42    ExecutionFailed { message: String },
43
44    /// Validation failed.
45    #[error("Validation failed:\n{errors}")]
46    ValidationFailed { errors: String },
47
48    /// IO error.
49    #[error("IO error: {0}")]
50    Io(#[from] std::io::Error),
51
52    /// YAML parsing error.
53    #[error("YAML parsing error: {0}")]
54    Yaml(#[from] serde_yaml::Error),
55
56    /// JSON parsing error.
57    #[error("JSON parsing error: {0}")]
58    Json(#[from] serde_json::Error),
59
60    /// Core simulator error.
61    #[error("Simulator error: {0}")]
62    Simulator(#[from] mabi_core::Error),
63
64    /// User interrupted operation.
65    #[error("Operation interrupted by user")]
66    Interrupted,
67
68    /// Timeout reached.
69    #[error("Operation timed out after {duration_secs} seconds")]
70    Timeout { duration_secs: u64 },
71
72    /// Generic error with context.
73    #[error("{context}: {source}")]
74    WithContext {
75        context: String,
76        #[source]
77        source: Box<CliError>,
78    },
79}
80
81impl CliError {
82    /// Add context to an error.
83    pub fn with_context(self, context: impl Into<String>) -> Self {
84        CliError::WithContext {
85            context: context.into(),
86            source: Box::new(self),
87        }
88    }
89
90    /// Create a validation failed error from multiple messages.
91    pub fn validation_failed(errors: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
92        let errors: Vec<String> = errors.into_iter().map(|s| format!("  - {}", s.as_ref())).collect();
93        CliError::ValidationFailed {
94            errors: errors.join("\n"),
95        }
96    }
97
98    /// Get the exit code for this error.
99    pub fn exit_code(&self) -> i32 {
100        match self {
101            CliError::ConfigNotFound { .. } => 2,
102            CliError::InvalidConfig { .. } => 2,
103            CliError::ScenarioNotFound { .. } => 2,
104            CliError::InvalidScenario { .. } => 2,
105            CliError::UnsupportedProtocol { .. } => 3,
106            CliError::DeviceNotFound { .. } => 4,
107            CliError::PortInUse { .. } => 5,
108            CliError::ExecutionFailed { .. } => 1,
109            CliError::ValidationFailed { .. } => 6,
110            CliError::Io(_) => 7,
111            CliError::Yaml(_) | CliError::Json(_) => 8,
112            CliError::Simulator(_) => 9,
113            CliError::Interrupted => 130,
114            CliError::Timeout { .. } => 124,
115            CliError::WithContext { source, .. } => source.exit_code(),
116        }
117    }
118}
119
120/// CLI result type alias.
121pub type CliResult<T> = Result<T, CliError>;
122
123/// Extension trait for adding CLI context to results.
124pub trait CliResultExt<T> {
125    /// Add context to a result error.
126    fn cli_context(self, context: impl Into<String>) -> CliResult<T>;
127}
128
129impl<T, E: Into<CliError>> CliResultExt<T> for Result<T, E> {
130    fn cli_context(self, context: impl Into<String>) -> CliResult<T> {
131        self.map_err(|e| e.into().with_context(context))
132    }
133}