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]
161 pub fn system(&self) -> Option<&str> {
162 match self {
163 Self::SystemMessage { system, .. }
164 | Self::System { system, .. }
165 | Self::Detailed { system, .. }
166 if !system.is_empty() =>
167 {
168 Some(system)
169 }
170 Self::MissingAuthProvider(_)
171 | Self::AuthProvider { .. }
172 | Self::InvalidOutputFormat(_)
173 | Self::Message(_)
174 | Self::SystemMessage { .. }
175 | Self::System { .. }
176 | Self::Detailed { .. }
177 | Self::ExitCode { .. }
178 | Self::Io(_)
179 | Self::Json(_)
180 | Self::Transport(_) => None,
181 }
182 }
183}
184
185impl ExitCoder for CliCoreError {
186 fn exit_code(&self) -> i32 {
187 exit_code_for_error(self)
188 }
189}
190
191#[must_use]
193pub fn exit_code_for_exit_coder(err: &dyn ExitCoder) -> i32 {
194 err.exit_code()
195}
196
197#[must_use]
199pub fn exit_code_for_error(err: &(dyn std::error::Error + 'static)) -> i32 {
200 let mut current = Some(err);
201 while let Some(error) = current {
202 if let Some(CliCoreError::ExitCode { code, .. }) = error.downcast_ref::<CliCoreError>() {
203 return *code;
204 }
205 current = error.source();
206 }
207
208 let mut current = Some(err);
209 while let Some(error) = current {
210 if let Some(cli_err) = error.downcast_ref::<CliCoreError>() {
211 match cli_err {
212 CliCoreError::MissingAuthProvider(_) | CliCoreError::AuthProvider { .. } => {
213 return 2;
214 }
215 CliCoreError::InvalidOutputFormat(_) => return 3,
216 CliCoreError::System { .. }
217 | CliCoreError::Detailed { .. }
218 | CliCoreError::ExitCode { .. }
219 | CliCoreError::Message(_)
220 | CliCoreError::SystemMessage { .. }
221 | CliCoreError::Io(_)
222 | CliCoreError::Json(_)
223 | CliCoreError::Transport(_) => {}
224 }
225 }
226 current = error.source();
227 }
228
229 let msg = err.to_string().to_lowercase();
230 if msg.contains("auth") {
231 2
232 } else if msg.contains("validation") || msg.contains("invalid") {
233 3
234 } else if msg.contains("not found") {
235 4
236 } else if msg.contains("permission") || msg.contains("forbidden") {
237 5
238 } else if msg.contains("denied") {
239 6
240 } else {
241 1
242 }
243}