use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CliError {
#[error("Configuration file not found: {path}")]
ConfigNotFound { path: PathBuf },
#[error("Invalid configuration: {message}")]
InvalidConfig { message: String },
#[error("Scenario file not found: {path}")]
ScenarioNotFound { path: PathBuf },
#[error("Invalid scenario: {message}")]
InvalidScenario { message: String },
#[error("Protocol not supported: {protocol}")]
UnsupportedProtocol { protocol: String },
#[error("Device not found: {device_id}")]
DeviceNotFound { device_id: String },
#[error("Port {port} is already in use. \
A previous mabi process may have been suspended (Ctrl+Z) and is still holding the port.\n \
Diagnostic: lsof -i :{port} | grep LISTEN\n \
To kill: kill $(lsof -ti :{port} -sTCP:LISTEN)")]
PortInUse { port: u16 },
#[error("Command execution failed: {message}")]
ExecutionFailed { message: String },
#[error("Validation failed:\n{errors}")]
ValidationFailed { errors: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("YAML parsing error: {0}")]
Yaml(#[from] serde_yaml::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
#[error("Simulator error: {0}")]
Simulator(#[from] mabi_core::Error),
#[error("Operation interrupted by user")]
Interrupted,
#[error("Operation timed out after {duration_secs} seconds")]
Timeout { duration_secs: u64 },
#[error("{context}: {source}")]
WithContext {
context: String,
#[source]
source: Box<CliError>,
},
}
impl CliError {
pub fn with_context(self, context: impl Into<String>) -> Self {
CliError::WithContext {
context: context.into(),
source: Box::new(self),
}
}
pub fn validation_failed(errors: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let errors: Vec<String> = errors
.into_iter()
.map(|s| format!(" - {}", s.as_ref()))
.collect();
CliError::ValidationFailed {
errors: errors.join("\n"),
}
}
pub fn exit_code(&self) -> i32 {
match self {
CliError::ConfigNotFound { .. } => 2,
CliError::InvalidConfig { .. } => 2,
CliError::ScenarioNotFound { .. } => 2,
CliError::InvalidScenario { .. } => 2,
CliError::UnsupportedProtocol { .. } => 3,
CliError::DeviceNotFound { .. } => 4,
CliError::PortInUse { .. } => 5,
CliError::ExecutionFailed { .. } => 1,
CliError::ValidationFailed { .. } => 6,
CliError::Io(_) => 7,
CliError::Yaml(_) | CliError::Json(_) => 8,
CliError::Simulator(_) => 9,
CliError::Interrupted => 130,
CliError::Timeout { .. } => 124,
CliError::WithContext { source, .. } => source.exit_code(),
}
}
}
impl From<mabi_runtime::RuntimeError> for CliError {
fn from(error: mabi_runtime::RuntimeError) -> Self {
CliError::ExecutionFailed {
message: error.to_string(),
}
}
}
pub type CliResult<T> = Result<T, CliError>;
pub trait CliResultExt<T> {
fn cli_context(self, context: impl Into<String>) -> CliResult<T>;
}
impl<T, E: Into<CliError>> CliResultExt<T> for Result<T, E> {
fn cli_context(self, context: impl Into<String>) -> CliResult<T> {
self.map_err(|e| e.into().with_context(context))
}
}