1use crate::errors::catalog::ErrorCode;
22
23use super::{ErrorPanel, OutputContext};
24
25pub fn display_error<E: std::error::Error>(error: &E, ctx: &OutputContext) {
34 tracing::debug!("Error occurred: {}", error);
36 let mut source = error.source();
37 while let Some(err) = source {
38 tracing::debug!(" Caused by: {}", err);
39 source = err.source();
40 }
41
42 if ctx.is_machine() {
44 return;
45 }
46
47 let panel = error_to_panel(error);
49
50 panel.render(*ctx);
52}
53
54pub fn display_error_with_code<E: std::error::Error>(
58 error: &E,
59 code: ErrorCode,
60 ctx: &OutputContext,
61) {
62 tracing::debug!("Error occurred [{}]: {}", code.code_string(), error);
64 let mut source = error.source();
65 while let Some(err) = source {
66 tracing::debug!(" Caused by: {}", err);
67 source = err.source();
68 }
69
70 if ctx.is_machine() {
72 return;
73 }
74
75 let entry = code.entry();
77 let mut panel = ErrorPanel::error(&entry.code, &entry.message).message(error.to_string());
78
79 let mut source = error.source();
81 while let Some(err) = source {
82 panel = panel.caused_by(err.to_string(), None);
83 source = err.source();
84 }
85
86 for step in entry.remediation {
88 panel = panel.suggestion(step);
89 }
90
91 panel.render(*ctx);
93}
94
95pub fn error_to_panel<E: std::error::Error>(error: &E) -> ErrorPanel {
100 let error_string = error.to_string();
101
102 let (code, title) = extract_error_info(&error_string);
104
105 let mut panel = ErrorPanel::error(&code, &title);
106
107 if title != error_string {
109 panel = panel.message(error_string);
110 }
111
112 let mut source = error.source();
114 while let Some(err) = source {
115 panel = panel.caused_by(err.to_string(), None);
116 source = err.source();
117 }
118
119 panel
120}
121
122fn extract_error_info(message: &str) -> (String, String) {
127 if let Some(caps) = extract_rch_code(message) {
129 return caps;
130 }
131
132 ("RCH-E500".to_string(), message.to_string())
134}
135
136fn extract_rch_code(message: &str) -> Option<(String, String)> {
138 let prefix = "RCH-E";
140 if let Some(start) = message.find(prefix) {
141 let code_start = start;
142 let after_prefix = start + prefix.len();
143
144 let code_end = message[after_prefix..]
146 .chars()
147 .take_while(|c| c.is_ascii_digit())
148 .count()
149 + after_prefix;
150
151 if code_end > after_prefix {
152 let code = message[code_start..code_end].to_string();
153
154 let rest = &message[code_end..];
156 let title = if let Some(stripped) = rest.strip_prefix(": ") {
157 stripped.to_string()
158 } else if let Some(stripped) = rest.strip_prefix("] ") {
159 stripped.to_string()
160 } else {
161 rest.trim_start_matches(&[' ', ':', ']'][..]).to_string()
162 };
163
164 return Some((
165 code,
166 if title.is_empty() {
167 message.to_string()
168 } else {
169 title
170 },
171 ));
172 }
173 }
174
175 None
176}
177
178pub fn anyhow_to_panel(error: &anyhow::Error) -> ErrorPanel {
180 let error_string = error.to_string();
181 let (code, title) = extract_error_info(&error_string);
182
183 let mut panel = ErrorPanel::error(&code, &title);
184
185 if title != error_string {
187 panel = panel.message(error_string);
188 }
189
190 for cause in error.chain().skip(1) {
192 panel = panel.caused_by(cause.to_string(), None);
193 }
194
195 panel
196}
197
198pub fn display_anyhow_error(error: &anyhow::Error, ctx: &OutputContext) {
200 tracing::debug!("Error occurred: {}", error);
202 for cause in error.chain().skip(1) {
203 tracing::debug!(" Caused by: {}", cause);
204 }
205
206 if ctx.is_machine() {
208 return;
209 }
210
211 let panel = anyhow_to_panel(error);
212 panel.render(*ctx);
213}
214
215pub fn error_to_json<E: std::error::Error>(error: &E) -> serde_json::Result<String> {
217 let panel = error_to_panel(error);
218 panel.to_json()
219}
220
221pub fn anyhow_to_json(error: &anyhow::Error) -> serde_json::Result<String> {
223 let panel = anyhow_to_panel(error);
224 panel.to_json()
225}
226
227pub trait IntoErrorPanel {
229 fn into_panel(self) -> ErrorPanel;
231}
232
233impl<E: std::error::Error> IntoErrorPanel for E {
234 fn into_panel(self) -> ErrorPanel {
235 error_to_panel(&self)
236 }
237}
238
239#[allow(clippy::result_unit_err)]
241pub trait ResultExt<T> {
242 fn display_err(self, ctx: &OutputContext) -> Result<T, ()>;
244
245 fn display_and_exit(self, ctx: &OutputContext, exit_code: i32) -> T;
247}
248
249impl<T, E: std::error::Error> ResultExt<T> for Result<T, E> {
250 fn display_err(self, ctx: &OutputContext) -> Result<T, ()> {
251 match self {
252 Ok(v) => Ok(v),
253 Err(e) => {
254 display_error(&e, ctx);
255 Err(())
256 }
257 }
258 }
259
260 fn display_and_exit(self, ctx: &OutputContext, exit_code: i32) -> T {
261 match self {
262 Ok(v) => v,
263 Err(e) => {
264 display_error(&e, ctx);
265 std::process::exit(exit_code);
266 }
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use std::io;
275
276 #[test]
277 fn test_extract_rch_code_with_colon() {
278 let result = extract_rch_code("RCH-E042: Worker Connection Failed");
279 assert_eq!(
280 result,
281 Some((
282 "RCH-E042".to_string(),
283 "Worker Connection Failed".to_string()
284 ))
285 );
286 }
287
288 #[test]
289 fn test_extract_rch_code_with_bracket() {
290 let result = extract_rch_code("[RCH-E100] SSH failed");
291 assert_eq!(
292 result,
293 Some(("RCH-E100".to_string(), "SSH failed".to_string()))
294 );
295 }
296
297 #[test]
298 fn test_extract_rch_code_no_code() {
299 let result = extract_rch_code("Some random error message");
300 assert_eq!(result, None);
301 }
302
303 #[test]
304 fn test_extract_error_info_with_code() {
305 let (code, title) = extract_error_info("RCH-E502: Daemon not running");
306 assert_eq!(code, "RCH-E502");
307 assert_eq!(title, "Daemon not running");
308 }
309
310 #[test]
311 fn test_extract_error_info_without_code() {
312 let (code, title) = extract_error_info("Something went wrong");
313 assert_eq!(code, "RCH-E500");
314 assert_eq!(title, "Something went wrong");
315 }
316
317 #[test]
318 fn test_error_to_panel_simple() {
319 let err = io::Error::new(io::ErrorKind::NotFound, "file not found");
320 let panel = error_to_panel(&err);
321
322 assert_eq!(panel.code, "RCH-E500");
323 assert!(panel.title.contains("not found"));
324 }
325
326 #[test]
327 fn test_error_to_panel_with_rch_code() {
328 #[derive(Debug)]
330 struct RchTestError;
331 impl std::fmt::Display for RchTestError {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 write!(f, "RCH-E042: Test error message")
334 }
335 }
336 impl std::error::Error for RchTestError {}
337
338 let err = RchTestError;
339 let panel = error_to_panel(&err);
340
341 assert_eq!(panel.code, "RCH-E042");
342 assert_eq!(panel.title, "Test error message");
343 }
344
345 #[test]
346 fn test_error_to_panel_with_source() {
347 let inner = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
348 let outer = io::Error::other(inner.to_string());
349
350 let panel = error_to_panel(&outer);
351
352 assert!(panel.title.contains("access denied"));
354 }
355
356 #[test]
357 fn test_display_error_machine_mode_silent() {
358 let err = io::Error::new(io::ErrorKind::NotFound, "test error");
359 let ctx = OutputContext::Machine;
360
361 display_error(&err, &ctx);
363 }
364
365 #[test]
366 fn test_display_error_plain_mode() {
367 let err = io::Error::new(io::ErrorKind::NotFound, "test error");
368 let ctx = OutputContext::Plain;
369
370 display_error(&err, &ctx);
372 }
373
374 #[test]
375 fn test_anyhow_to_panel() {
376 let err = anyhow::anyhow!("RCH-E100: SSH connection failed");
377 let panel = anyhow_to_panel(&err);
378
379 assert_eq!(panel.code, "RCH-E100");
380 assert_eq!(panel.title, "SSH connection failed");
381 }
382
383 #[test]
384 fn test_anyhow_to_panel_with_context() {
385 let err = anyhow::anyhow!("inner error").context("outer context");
386 let panel = anyhow_to_panel(&err);
387
388 assert!(panel.title.contains("outer context") || panel.message.is_some());
390 }
391
392 #[test]
393 fn test_error_to_json() {
394 let err = io::Error::new(io::ErrorKind::NotFound, "file not found");
395 let json = error_to_json(&err).expect("JSON serialization failed");
396
397 assert!(json.contains("RCH-E500"));
398 assert!(json.contains("not found"));
399 }
400
401 #[test]
402 fn test_anyhow_to_json() {
403 let err = anyhow::anyhow!("test error");
404 let json = anyhow_to_json(&err).expect("JSON serialization failed");
405
406 assert!(json.contains("RCH-E500"));
407 assert!(json.contains("test error"));
408 }
409
410 #[test]
411 fn test_result_ext_display_err_ok() {
412 let result: Result<i32, io::Error> = Ok(42);
413 let ctx = OutputContext::Plain;
414
415 assert_eq!(result.display_err(&ctx), Ok(42));
416 }
417
418 #[test]
419 fn test_result_ext_display_err_error() {
420 let result: Result<i32, io::Error> =
421 Err(io::Error::new(io::ErrorKind::NotFound, "not found"));
422 let ctx = OutputContext::Plain;
423
424 assert_eq!(result.display_err(&ctx), Err(()));
425 }
426}