1use std::path::PathBuf;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8pub enum CliError {
9 #[error("Configuration file not found: {path}")]
11 ConfigNotFound { path: PathBuf },
12
13 #[error("Invalid configuration: {message}")]
15 InvalidConfig { message: String },
16
17 #[error("Scenario file not found: {path}")]
19 ScenarioNotFound { path: PathBuf },
20
21 #[error("Invalid scenario: {message}")]
23 InvalidScenario { message: String },
24
25 #[error("Protocol not supported: {protocol}")]
27 UnsupportedProtocol { protocol: String },
28
29 #[error("Device not found: {device_id}")]
31 DeviceNotFound { device_id: String },
32
33 #[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 #[error("Command execution failed: {message}")]
42 ExecutionFailed { message: String },
43
44 #[error("Validation failed:\n{errors}")]
46 ValidationFailed { errors: String },
47
48 #[error("IO error: {0}")]
50 Io(#[from] std::io::Error),
51
52 #[error("YAML parsing error: {0}")]
54 Yaml(#[from] serde_yaml::Error),
55
56 #[error("JSON parsing error: {0}")]
58 Json(#[from] serde_json::Error),
59
60 #[error("Simulator error: {0}")]
62 Simulator(#[from] mabi_core::Error),
63
64 #[error("Operation interrupted by user")]
66 Interrupted,
67
68 #[error("Operation timed out after {duration_secs} seconds")]
70 Timeout { duration_secs: u64 },
71
72 #[error("{context}: {source}")]
74 WithContext {
75 context: String,
76 #[source]
77 source: Box<CliError>,
78 },
79}
80
81impl CliError {
82 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 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 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
120pub type CliResult<T> = Result<T, CliError>;
122
123pub trait CliResultExt<T> {
125 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}