1use std::borrow::Cow;
2
3use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, CliCoreError>;
7
8pub trait ExitCoder {
10 fn exit_code(&self) -> i32;
12}
13
14pub trait DetailedError: std::error::Error {
16 fn error_code(&self) -> Cow<'static, str>;
18 fn error_system(&self) -> Option<Cow<'static, str>>;
20 fn error_request_id(&self) -> Option<Cow<'static, str>>;
22}
23
24#[derive(Debug, Error)]
26pub enum CliCoreError {
27 #[error("auth: no provider registered with name {0:?}")]
29 MissingAuthProvider(String),
30 #[error("auth: provider {provider:?}: {source}")]
32 AuthProvider {
33 provider: String,
35 #[source]
37 source: Box<dyn std::error::Error + Send + Sync>,
38 },
39 #[error("invalid output format {0:?}: must be one of toon, json, human")]
41 InvalidOutputFormat(String),
42 #[error("{0}")]
44 Message(String),
45 #[error("{message}")]
47 SystemMessage {
48 message: String,
50 system: String,
52 code: String,
54 request_id: String,
56 },
57 #[error("{source}")]
59 System {
60 system: String,
62 #[source]
64 source: Box<dyn std::error::Error + Send + Sync>,
65 },
66 #[error("{source}")]
68 Detailed {
69 code: String,
71 system: String,
73 request_id: String,
75 #[source]
77 source: Box<dyn std::error::Error + Send + Sync>,
78 },
79 #[error("{source}")]
81 ExitCode {
82 code: i32,
84 #[source]
86 source: Box<dyn std::error::Error + Send + Sync>,
87 },
88 #[error(transparent)]
90 Io(#[from] std::io::Error),
91 #[error(transparent)]
93 Json(#[from] serde_json::Error),
94 #[error(transparent)]
96 Transport(#[from] crate::transport::Error),
97}
98
99impl CliCoreError {
100 #[must_use]
102 pub fn message(message: impl Into<String>) -> Self {
103 Self::Message(message.into())
104 }
105
106 #[must_use]
108 pub fn message_for_system(system: impl Into<String>, message: impl Into<String>) -> Self {
109 Self::SystemMessage {
110 message: message.into(),
111 system: system.into(),
112 code: "ERROR".to_owned(),
113 request_id: String::new(),
114 }
115 }
116
117 #[must_use]
119 pub fn with_system(
120 system: impl Into<String>,
121 source: impl std::error::Error + Send + Sync + 'static,
122 ) -> Self {
123 Self::System {
124 system: system.into(),
125 source: Box::new(source),
126 }
127 }
128
129 #[must_use]
131 pub fn with_exit_code(
132 code: i32,
133 source: impl std::error::Error + Send + Sync + 'static,
134 ) -> Self {
135 Self::ExitCode {
136 code,
137 source: Box::new(source),
138 }
139 }
140
141 #[must_use]
143 pub fn with_detailed_error(source: impl DetailedError + Send + Sync + 'static) -> Self {
144 let code = source.error_code().into_owned();
145 let system = source
146 .error_system()
147 .map_or_else(String::new, Cow::into_owned);
148 let request_id = source
149 .error_request_id()
150 .map_or_else(String::new, Cow::into_owned);
151 Self::Detailed {
152 code,
153 system,
154 request_id,
155 source: Box::new(source),
156 }
157 }
158
159 #[must_use]
167 pub fn is_auth(&self) -> bool {
168 matches!(
169 self,
170 Self::MissingAuthProvider(_) | Self::AuthProvider { .. }
171 )
172 }
173
174 #[must_use]
176 pub fn system(&self) -> Option<&str> {
177 match self {
178 Self::SystemMessage { system, .. }
179 | Self::System { system, .. }
180 | Self::Detailed { system, .. }
181 if !system.is_empty() =>
182 {
183 Some(system)
184 }
185 Self::MissingAuthProvider(_)
186 | Self::AuthProvider { .. }
187 | Self::InvalidOutputFormat(_)
188 | Self::Message(_)
189 | Self::SystemMessage { .. }
190 | Self::System { .. }
191 | Self::Detailed { .. }
192 | Self::ExitCode { .. }
193 | Self::Io(_)
194 | Self::Json(_)
195 | Self::Transport(_) => None,
196 }
197 }
198}
199
200impl ExitCoder for CliCoreError {
201 fn exit_code(&self) -> i32 {
202 exit_code_for_error(self)
203 }
204}
205
206#[must_use]
208pub fn exit_code_for_exit_coder(err: &dyn ExitCoder) -> i32 {
209 err.exit_code()
210}
211
212#[must_use]
214pub fn exit_code_for_error(err: &(dyn std::error::Error + 'static)) -> i32 {
215 let mut current = Some(err);
216 while let Some(error) = current {
217 if let Some(CliCoreError::ExitCode { code, .. }) = error.downcast_ref::<CliCoreError>() {
218 return *code;
219 }
220 current = error.source();
221 }
222
223 let mut current = Some(err);
224 while let Some(error) = current {
225 if let Some(cli_err) = error.downcast_ref::<CliCoreError>() {
226 match cli_err {
227 CliCoreError::MissingAuthProvider(_) | CliCoreError::AuthProvider { .. } => {
228 return 2;
229 }
230 CliCoreError::InvalidOutputFormat(_) => return 3,
231 CliCoreError::System { .. }
232 | CliCoreError::Detailed { .. }
233 | CliCoreError::ExitCode { .. }
234 | CliCoreError::Message(_)
235 | CliCoreError::SystemMessage { .. }
236 | CliCoreError::Io(_)
237 | CliCoreError::Json(_)
238 | CliCoreError::Transport(_) => {}
239 }
240 }
241 current = error.source();
242 }
243
244 let msg = err.to_string().to_lowercase();
245 if msg.contains("auth") {
246 2
247 } else if msg.contains("validation") || msg.contains("invalid") {
248 3
249 } else if msg.contains("not found") {
250 4
251 } else if msg.contains("permission") || msg.contains("forbidden") {
252 5
253 } else if msg.contains("denied") {
254 6
255 } else {
256 1
257 }
258}