user_error/lib.rs
1//! # User Facing Error
2//! A library for conveniently displaying well-formatted, and good looking
3//! errors to users of CLI applications. Useful for bubbling up unrecoverable
4//! errors to inform the user what they can do to fix them. Error messages you'd
5//! be proud to show your mom.
6#![deny(
7 missing_docs,
8 missing_debug_implementations,
9 missing_copy_implementations,
10 trivial_casts,
11 trivial_numeric_casts,
12 unstable_features,
13 unsafe_code,
14 unused_import_braces,
15 unused_qualifications
16)]
17
18// Standard Library Dependencies
19use core::fmt::{self, Debug, Display};
20use std::error::Error;
21
22/*************
23 * CONSTANTS *
24 *************/
25
26// 'Error:' with a red background and white, bold, text
27const SUMMARY_PREFIX: &str = "\u{001b}[97;41;22mError:\u{001b}[91;49;1m ";
28// ' - ' bullet point in yellow and text in bold white
29const REASON_PREFIX: &str = "\u{001b}[93;49;1m - \u{001b}[97;49;1m";
30// Muted white help text
31const HELPTEXT_PREFIX: &str = "\u{001b}[37;49;2m";
32// ASCII Reset formatting escape code
33const RESET: &str = "\u{001b}[0m";
34
35// Helper function to keep things DRY
36// Takes a dyn Error.source() and returns a Vec of Strings representing all the
37// .sources() in the error chain (if any)
38fn error_sources(mut source: Option<&(dyn Error + 'static)>) -> Option<Vec<String>> {
39 /* Check if we have any sources to derive reasons from */
40 if source.is_some() {
41 /* Add all the error sources to a list of reasons for the error */
42 let mut reasons = Vec::new();
43 while let Some(error) = source {
44 reasons.push(error.to_string());
45 source = error.source();
46 }
47 Some(reasons)
48 } else {
49 None
50 }
51}
52
53/*********
54 * TRAIT *
55 *********/
56
57// Helper Functions
58
59/// Convenience function that converts the summary into pretty String.
60fn pretty_summary(summary: &str) -> String {
61 [SUMMARY_PREFIX, summary, RESET].concat()
62}
63
64/// Convenience function that converts the reasons into pretty String.
65fn pretty_reasons(reasons: Reasons) -> Option<String> {
66 /* Print list of Reasons (if any) */
67 if let Some(reasons) = reasons {
68 /* Vector to store the intermediate bullet point strings */
69 let mut reason_strings = Vec::with_capacity(reasons.len());
70 for reason in reasons {
71 let bullet_point = [REASON_PREFIX, &reason].concat();
72 reason_strings.push(bullet_point);
73 }
74 /* Join the buller points with a newline, append a RESET ASCII escape code to the end */
75 Some([&reason_strings.join("\n"), RESET].concat())
76 } else {
77 None
78 }
79}
80
81/// Convenience function that converts the help text into pretty String.
82fn pretty_helptext(helptext: Helptext) -> Option<String> {
83 if let Some(helptext) = helptext {
84 Some([HELPTEXT_PREFIX, &helptext, RESET].concat())
85 } else {
86 None
87 }
88}
89
90/// You can implement UFE on your error types pretty print them. The default
91/// implementation will print Error: <your error .to_string()> followed by a list
92/// of reasons that are any errors returned by .source(). You should only
93/// override the summary, reasons and help text functions. The pretty print
94/// versions of these are used by print(), print_and_exit() and contain the
95/// formatting. If you wish to change the formatting you should update it with
96/// the formatting functions.
97pub trait UFE: Error {
98 /**************
99 * IMPLENT ME *
100 **************/
101
102 /// Returns a summary of the error. This will be printed in red, prefixed
103 /// by "Error: ", at the top of the error message.
104 fn summary(&self) -> String {
105 self.to_string()
106 }
107
108 /// Returns a vector of Strings that will be listed as bullet points below
109 /// the summary. By default, lists any errors returned by .source()
110 /// recursively.
111 fn reasons(&self) -> Option<Vec<String>> {
112 /* Helper function to keep things DRY */
113 error_sources(self.source())
114 }
115
116 /// Returns help text that is listed below the reasons in a muted fashion.
117 /// Useful for additional details, or suggested next steps.
118 fn helptext(&self) -> Option<String> {
119 None
120 }
121
122 /**********
123 * USE ME *
124 **********/
125
126 /// Prints the formatted error.
127 /// # Example
128 /// ```
129 /// use user_error::{UserFacingError, UFE};
130 /// UserFacingError::new("File failed to open")
131 /// .reason("File not found")
132 /// .help("Try: touch file.txt")
133 /// .print();
134 /// ```
135 fn print(&self) {
136 /* Print Summary */
137 eprintln!("{}", pretty_summary(&self.summary()));
138
139 /* Print list of Reasons (if any) */
140 if let Some(reasons) = pretty_reasons(self.reasons()) {
141 eprintln!("{}", reasons);
142 }
143
144 /* Print help text (if any) */
145 if let Some(helptext) = pretty_helptext(self.helptext()) {
146 eprintln!("{}", helptext);
147 }
148 }
149
150 /// Convenience function that pretty prints the error and exits the program.
151 /// # Example
152 /// ```should_panic
153 /// use user_error::{UserFacingError, UFE};
154 /// UserFacingError::new("File failed to open")
155 /// .reason("File not found")
156 /// .help("Try: touch file.txt")
157 /// .print_and_exit();
158 /// ```
159 fn print_and_exit(&self) {
160 self.print();
161 std::process::exit(1)
162 }
163
164 /// Consumes the UFE and returns a UserFacingError. Useful if you want
165 /// access to additional functions to edit the error message before exiting
166 /// the program.
167 /// # Example
168 /// ```
169 /// use user_error::{UserFacingError, UFE};
170 /// use std::fmt::{self, Display};
171 /// use std::error::Error;
172 ///
173 /// #[derive(Debug)]
174 /// struct MyError {}
175 ///
176 /// impl Display for MyError {
177 /// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 /// write!(f, "MyError")
179 /// }
180 /// }
181 ///
182 /// impl Error for MyError {
183 /// fn source(&self) -> Option<&(dyn Error + 'static)> { None }
184 /// }
185 ///
186 /// impl UFE for MyError {}
187 ///
188 /// fn main() {
189 /// let me = MyError {};
190 /// me.print();
191 /// me.into_ufe()
192 /// .help("Added help text")
193 /// .print();
194 /// }
195 /// ```
196 fn into_ufe(&self) -> UserFacingError {
197 UserFacingError {
198 summary: self.summary(),
199 reasons: self.reasons(),
200 helptext: self.helptext(),
201 source: None,
202 }
203 }
204}
205
206/**********
207 * STRUCT *
208 **********/
209type Summary = String;
210type Reasons = Option<Vec<String>>;
211type Helptext = Option<String>;
212type Source = Option<Box<(dyn Error)>>;
213
214/// The eponymous struct. You can create a new one from using
215/// user_error::UserFacingError::new() however I recommend you use your own
216/// error types and have them implement UFE instead of using UserFacingError
217/// directly. This is more of an example type, or a way to construct a pretty
218/// messages without implementing your own error type.
219#[derive(Debug)]
220pub struct UserFacingError {
221 summary: Summary,
222 reasons: Reasons,
223 helptext: Helptext,
224 source: Source,
225}
226
227/******************
228 * IMPLEMENTATION *
229 ******************/
230
231// Implement Display so our struct also implements std::error::Error
232impl Display for UserFacingError {
233 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
234 let summary = pretty_summary(&self.summary());
235 let reasons = pretty_reasons(self.reasons());
236 let helptext = pretty_helptext(self.helptext());
237
238 // Love this - thanks Rust!
239 match (summary, reasons, helptext) {
240 (summary, None, None) => writeln!(f, "{}", summary),
241 (summary, Some(reasons), None) => writeln!(f, "{}\n{}", summary, reasons),
242 (summary, None, Some(helptext)) => writeln!(f, "{}\n{}", summary, helptext),
243 (summary, Some(reasons), Some(helptext)) => {
244 writeln!(f, "{}\n{}\n{}", summary, reasons, helptext)
245 }
246 }
247 }
248}
249
250// Implement std::error::Error
251impl Error for UserFacingError {
252 fn source(&self) -> Option<&(dyn Error + 'static)> {
253 match self.source {
254 Some(_) => self.source.as_deref(),
255 None => None,
256 }
257 }
258}
259
260// Implement our own trait for our example struct
261// Cloning is not super efficient but this should be the last thing a program
262// does, and it will only do it once so... ¯\_(ツ)_/¯
263impl UFE for UserFacingError {
264 fn summary(&self) -> Summary {
265 self.summary.clone()
266 }
267 fn reasons(&self) -> Reasons {
268 self.reasons.clone()
269 }
270 fn helptext(&self) -> Helptext {
271 self.helptext.clone()
272 }
273}
274
275// Helper function to keep things DRY
276fn get_ufe_struct_members(error: &(dyn Error)) -> (Summary, Reasons) {
277 /* Error Display format is the summary */
278 let summary = error.to_string();
279 /* Form the reasons from the error source chain */
280 let reasons = error_sources(error.source());
281 (summary, reasons)
282}
283
284/// Allows you to create UserFacingErrors From std::io::Error for convenience
285/// You should really just implement UFE for your error type, but if you wanted
286/// to convert before quitting so you could add help text of something you can
287/// use this.
288impl From<std::io::Error> for UserFacingError {
289 fn from(error: std::io::Error) -> UserFacingError {
290 let (summary, reasons) = get_ufe_struct_members(&error);
291
292 UserFacingError {
293 summary,
294 reasons,
295 helptext: None,
296 source: Some(Box::new(error)),
297 }
298 }
299}
300
301/// Allows you to create UserFacingErrors From std Errors.
302/// You should really just implement UFE for your error type, but if you wanted
303/// to convert before quitting so you could add help text of something you can
304/// use this.
305impl From<Box<(dyn Error)>> for UserFacingError {
306 fn from(error: Box<(dyn Error)>) -> UserFacingError {
307 let (summary, reasons) = get_ufe_struct_members(error.as_ref());
308
309 UserFacingError {
310 summary,
311 reasons,
312 helptext: None,
313 source: Some(error),
314 }
315 }
316}
317
318/// Allows you to create UserFacingErrors From std Errors.
319/// You should really just implement UFE for your error type, but if you wanted
320/// to convert before quitting so you could add help text of something you can
321/// use this.
322impl From<&(dyn Error)> for UserFacingError {
323 fn from(error: &(dyn Error)) -> UserFacingError {
324 let (summary, reasons) = get_ufe_struct_members(error);
325
326 UserFacingError {
327 summary,
328 reasons,
329 helptext: None,
330 source: None,
331 }
332 }
333}
334
335/// Allows you to create UserFacingErrors From std Errors wrapped in a Result
336/// You should really just implement UFE for your error type, but if you wanted
337/// to convert before quitting so you could add help text of something you can
338/// use this.
339impl<T: Debug> From<Result<T, Box<dyn Error>>> for UserFacingError {
340 fn from(error: Result<T, Box<dyn Error>>) -> UserFacingError {
341 /* Panics if you try to convert an Ok() Result to a UserFacingError */
342 let error = error.unwrap_err();
343 let (summary, reasons) = get_ufe_struct_members(error.as_ref());
344
345 UserFacingError {
346 summary,
347 reasons,
348 helptext: None,
349 source: Some(error),
350 }
351 }
352}
353
354impl UserFacingError {
355 /// This is how users create a new User Facing Error. The value passed to
356 /// new() will be used as an error summary. Error summaries are displayed
357 /// first, prefixed by 'Error: '.
358 /// # Example
359 /// ```
360 /// # use user_error::UserFacingError;
361 /// let err = UserFacingError::new("File failed to open");
362 /// ```
363 pub fn new<S: Into<String>>(summary: S) -> UserFacingError {
364 UserFacingError {
365 summary: summary.into(),
366 reasons: None,
367 helptext: None,
368 source: None,
369 }
370 }
371
372 /// Replace the error summary.
373 /// # Example
374 /// ```
375 /// # use user_error::UserFacingError;
376 /// let mut err = UserFacingError::new("File failed to open");
377 /// err.update("Failed Task");
378 /// ```
379 pub fn update<S: Into<String>>(&mut self, summary: S) {
380 self.summary = summary.into();
381 }
382
383 /// Replace the error summary and add the previous error summary to the
384 /// list of reasons
385 /// # Example
386 /// ```
387 /// # use user_error::UserFacingError;
388 /// let mut err = UserFacingError::new("File failed to open");
389 /// err.push("Failed Task");
390 /// ```
391 pub fn push<S: Into<String>>(&mut self, new_summary: S) {
392 // Add the old summary to the list of reasons
393 let old_summary = self.summary();
394 match self.reasons.as_mut() {
395 Some(reasons) => reasons.insert(0, old_summary),
396 None => self.reasons = Some(vec![old_summary]),
397 }
398
399 // Update the summary
400 self.summary = new_summary.into();
401 }
402
403 /// Add a reason to the UserFacingError. Reasons are displayed in a
404 /// bulleted list below the summary, in the reverse order they were added.
405 /// # Example
406 /// ```
407 /// # use user_error::UserFacingError;
408 /// let err = UserFacingError::new("File failed to open")
409 /// .reason("File not found")
410 /// .reason("Directory cannot be entered");
411 /// ```
412 pub fn reason<S: Into<String>>(mut self, reason: S) -> UserFacingError {
413 self.reasons = match self.reasons {
414 Some(mut reasons) => {
415 reasons.push(reason.into());
416 Some(reasons)
417 }
418 None => Some(vec![reason.into()]),
419 };
420 self
421 }
422
423 // Return ref to previous?
424
425 /// Clears all reasons from a UserFacingError.
426 /// # Example
427 /// ```
428 /// # use user_error::UserFacingError;
429 /// let mut err = UserFacingError::new("File failed to open")
430 /// .reason("File not found")
431 /// .reason("Directory cannot be entered");
432 /// err.clear_reasons();
433 /// ```
434 pub fn clear_reasons(&mut self) {
435 self.reasons = None;
436 }
437
438 /// Add help text to the error. Help text is displayed last, in a muted
439 /// fashion.
440 /// # Example
441 /// ```
442 /// # use user_error::UserFacingError;
443 /// let err = UserFacingError::new("File failed to open")
444 /// .reason("File not found")
445 /// .help("Check if the file exists.");
446 /// ```
447 pub fn help<S: Into<String>>(mut self, helptext: S) -> UserFacingError {
448 self.helptext = Some(helptext.into());
449 self
450 }
451
452 /// Clears all the help text from a UserFacingError.
453 /// # Example
454 /// ```
455 /// # use user_error::UserFacingError;
456 /// let mut err = UserFacingError::new("File failed to open")
457 /// .reason("File not found")
458 /// .reason("Directory cannot be entered")
459 /// .help("Check if the file exists.");
460 /// err.clear_helptext();
461 /// ```
462 pub fn clear_helptext(&mut self) {
463 self.helptext = None;
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 // Statics to keep the testing DRY/cleaner
471 static S: &'static str = "Test Error";
472 static R: &'static str = "Reason 1";
473 static H: &'static str = "Try Again";
474
475 #[test]
476 fn new_test() {
477 eprintln!("{}", UserFacingError::new("Test Error"));
478 }
479
480 #[test]
481 fn summary_test() {
482 let e = UserFacingError::new(S);
483 let expected = [SUMMARY_PREFIX, S, RESET, "\n"].concat();
484 assert_eq!(e.to_string(), String::from(expected));
485 eprintln!("{}", e);
486 }
487
488 #[test]
489 fn helptext_test() {
490 let e = UserFacingError::new(S).help(H);
491 let expected = format!(
492 "{}{}{}\n{}{}{}\n",
493 SUMMARY_PREFIX, S, RESET, HELPTEXT_PREFIX, H, RESET
494 );
495 assert_eq!(e.to_string(), expected);
496 eprintln!("{}", e);
497 }
498
499 #[test]
500 fn reason_test() {
501 let e = UserFacingError::new(S).reason(R).reason(R);
502
503 /* Create Reasons String */
504 let reasons = vec![String::from(R), String::from(R)];
505 let mut reason_strings = Vec::with_capacity(reasons.len());
506 for reason in reasons {
507 let bullet_point = [REASON_PREFIX, &reason].concat();
508 reason_strings.push(bullet_point);
509 }
510 // Join the bullet points with a newline, append a RESET ASCII escape
511 // code to the end.
512 let reasons = [&reason_strings.join("\n"), RESET].concat();
513
514 let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, S, RESET, reasons);
515 assert_eq!(e.to_string(), expected);
516 eprintln!("{}", e);
517 }
518
519 #[test]
520 fn push_test() {
521 let mut e = UserFacingError::new(S).reason("R1");
522 e.push("R2");
523
524 /* Create Reasons String */
525 let reasons = vec![String::from(S), String::from("R1")];
526 let mut reason_strings = Vec::with_capacity(reasons.len());
527 for reason in reasons {
528 let bullet_point = [REASON_PREFIX, &reason].concat();
529 reason_strings.push(bullet_point);
530 }
531 // Join the bullet points with a newline, append a RESET ASCII escape
532 // code to the end
533 let reasons = [&reason_strings.join("\n"), RESET].concat();
534
535 let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, "R2", RESET, reasons);
536 assert_eq!(e.to_string(), expected);
537 eprintln!("{}", e);
538 }
539
540 #[test]
541 fn push_test_empty() {
542 let mut e = UserFacingError::new(S);
543 e.push("S2");
544
545 // Create Reasons String
546 let reasons = vec![String::from(S)];
547 let mut reason_strings = Vec::with_capacity(reasons.len());
548 for reason in reasons {
549 let bullet_point = [REASON_PREFIX, &reason].concat();
550 reason_strings.push(bullet_point);
551 }
552 // Join the bullet points with a newline, append a RESET ASCII escape
553 // code to the end
554 let reasons = [&reason_strings.join("\n"), RESET].concat();
555
556 let expected = format!("{}{}{}\n{}\n", SUMMARY_PREFIX, "S2", RESET, reasons);
557 assert_eq!(e.to_string(), expected);
558 eprintln!("{}", e);
559 }
560
561 #[test]
562 fn reason_and_helptext_test() {
563 let e = UserFacingError::new(S).reason(R).reason(R).help(H);
564
565 // Create Reasons String
566 let reasons = vec![String::from(R), String::from(R)];
567 let mut reason_strings = Vec::with_capacity(reasons.len());
568 for reason in reasons {
569 let bullet_point = [REASON_PREFIX, &reason].concat();
570 reason_strings.push(bullet_point);
571 }
572
573 // Join the bullet points with a newline, append a RESET ASCII escape
574 // code to the end
575 let reasons = [&reason_strings.join("\n"), RESET].concat();
576
577 let expected = format!(
578 "{}{}{}\n{}\n{}{}{}\n",
579 SUMMARY_PREFIX, S, RESET, reasons, HELPTEXT_PREFIX, H, RESET
580 );
581 assert_eq!(e.to_string(), expected);
582 eprintln!("{}", e);
583 }
584
585 #[test]
586 fn from_error_test() {
587 let error_text = "Error";
588 let ioe = std::io::Error::new(std::io::ErrorKind::Other, error_text);
589
590 // Lose the type
591 fn de(ioe: std::io::Error) -> Box<dyn Error> {
592 Box::new(ioe)
593 }
594 // Convert to UFE
595 let ufe: UserFacingError = de(ioe).into();
596
597 let expected = [SUMMARY_PREFIX, error_text, RESET, "\n"].concat();
598 assert_eq!(ufe.to_string(), expected);
599 }
600
601 #[test]
602 fn from_error_source_test() {
603 let ufe: UserFacingError = get_super_error().into();
604 let expected = [
605 SUMMARY_PREFIX,
606 "SuperError",
607 RESET,
608 "\n",
609 REASON_PREFIX,
610 "Sidekick",
611 RESET,
612 "\n",
613 ]
614 .concat();
615
616 assert_eq!(ufe.to_string(), expected);
617 }
618
619 // Used for to test that source is working correctly
620 #[derive(Debug)]
621 struct SuperError {
622 side: SuperErrorSideKick,
623 }
624
625 impl Display for SuperError {
626 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627 write!(f, "SuperError")
628 }
629 }
630
631 impl Error for SuperError {
632 fn source(&self) -> Option<&(dyn Error + 'static)> {
633 Some(&self.side)
634 }
635 }
636
637 #[derive(Debug)]
638 struct SuperErrorSideKick;
639
640 impl Display for SuperErrorSideKick {
641 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
642 write!(f, "Sidekick")
643 }
644 }
645
646 impl Error for SuperErrorSideKick {
647 fn source(&self) -> Option<&(dyn Error + 'static)> {
648 None
649 }
650 }
651
652 fn get_super_error() -> Result<(), Box<dyn Error>> {
653 Err(Box::new(SuperError {
654 side: SuperErrorSideKick,
655 }))
656 }
657
658 // Custom Error Type
659 #[derive(Debug)]
660 struct MyError {
661 mssg: String,
662 src: Option<Box<dyn Error>>,
663 }
664
665 impl Display for MyError {
666 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667 write!(f, "{}", self.mssg.to_string())
668 }
669 }
670
671 impl Error for MyError {
672 fn source(&self) -> Option<&(dyn Error + 'static)> {
673 self.src.as_deref()
674 }
675 }
676
677 impl UFE for MyError {}
678
679 #[test]
680 fn custom_error_implements_ufe() {
681 let me = MyError {
682 mssg: "Program Failed".into(),
683 src: Some(Box::new(MyError {
684 mssg: "Reason 1".into(),
685 src: Some(Box::new(MyError {
686 mssg: "Reason 2".into(),
687 src: None,
688 })),
689 })),
690 };
691 me.print();
692 me.into_ufe().help("Helptext Added").print();
693 }
694}