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
93            .into_iter()
94            .map(|s| format!("  - {}", s.as_ref()))
95            .collect();
96        CliError::ValidationFailed {
97            errors: errors.join("\n"),
98        }
99    }
100
101    /// Get the exit code for this error.
102    pub fn exit_code(&self) -> i32 {
103        match self {
104            CliError::ConfigNotFound { .. } => 2,
105            CliError::InvalidConfig { .. } => 2,
106            CliError::ScenarioNotFound { .. } => 2,
107            CliError::InvalidScenario { .. } => 2,
108            CliError::UnsupportedProtocol { .. } => 3,
109            CliError::DeviceNotFound { .. } => 4,
110            CliError::PortInUse { .. } => 5,
111            CliError::ExecutionFailed { .. } => 1,
112            CliError::ValidationFailed { .. } => 6,
113            CliError::Io(_) => 7,
114            CliError::Yaml(_) | CliError::Json(_) => 8,
115            CliError::Simulator(_) => 9,
116            CliError::Interrupted => 130,
117            CliError::Timeout { .. } => 124,
118            CliError::WithContext { source, .. } => source.exit_code(),
119        }
120    }
121}
122
123impl From<mabi_runtime::RuntimeError> for CliError {
124    fn from(error: mabi_runtime::RuntimeError) -> Self {
125        CliError::ExecutionFailed {
126            message: error.to_string(),
127        }
128    }
129}
130
131/// CLI result type alias.
132pub type CliResult<T> = Result<T, CliError>;
133
134/// Extension trait for adding CLI context to results.
135pub trait CliResultExt<T> {
136    /// Add context to a result error.
137    fn cli_context(self, context: impl Into<String>) -> CliResult<T>;
138}
139
140impl<T, E: Into<CliError>> CliResultExt<T> for Result<T, E> {
141    fn cli_context(self, context: impl Into<String>) -> CliResult<T> {
142        self.map_err(|e| e.into().with_context(context))
143    }
144}