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}