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 PortInUse { port: u16 },
36
37 #[error("Command execution failed: {message}")]
39 ExecutionFailed { message: String },
40
41 #[error("Validation failed:\n{errors}")]
43 ValidationFailed { errors: String },
44
45 #[error("IO error: {0}")]
47 Io(#[from] std::io::Error),
48
49 #[error("YAML parsing error: {0}")]
51 Yaml(#[from] serde_yaml::Error),
52
53 #[error("JSON parsing error: {0}")]
55 Json(#[from] serde_json::Error),
56
57 #[error("Simulator error: {0}")]
59 Simulator(#[from] mabi_core::Error),
60
61 #[error("Operation interrupted by user")]
63 Interrupted,
64
65 #[error("Operation timed out after {duration_secs} seconds")]
67 Timeout { duration_secs: u64 },
68
69 #[error("{context}: {source}")]
71 WithContext {
72 context: String,
73 #[source]
74 source: Box<CliError>,
75 },
76}
77
78impl CliError {
79 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 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 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
117pub type CliResult<T> = Result<T, CliError>;
119
120pub trait CliResultExt<T> {
122 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}