human_errors/
error.rs

1use super::Kind;
2use std::{error, fmt};
3
4#[cfg(feature = "serde")]
5use serde::ser::SerializeStruct;
6
7/// The fundamental error type used by this library.
8///
9/// An error type which encapsulates information about whether an error
10/// is the result of something the user did, or a system failure outside
11/// their control. These errors include a description of what occurred,
12/// advice on how to proceed and references to the causal chain which led
13/// to this failure.
14///
15/// # Examples
16/// ```
17/// use human_errors;
18///
19/// let err = human_errors::user(
20///   "We could not open the config file you provided.",
21///   &["Make sure that the file exists and is readable by the application."],
22/// );
23///
24/// // Prints the error and any advice for the user.
25/// println!("{}", err)
26/// ```
27#[derive(Debug)]
28pub struct Error {
29    pub(crate) kind: Kind,
30    pub(crate) error: Box<dyn error::Error + Send + Sync>,
31    pub(crate) advice: &'static [&'static str],
32}
33
34impl Error {
35    /// Constructs a new [Error].
36    ///
37    /// # Examples
38    /// ```
39    /// use human_errors;
40    /// let err = human_errors::Error::new(
41    ///     "Low-level IO error details",
42    ///     human_errors::Kind::System,
43    ///     &["Try restarting the application", "If the problem persists, contact support"]
44    /// );
45    /// ```
46    pub fn new<E: Into<Box<dyn error::Error + Send + Sync>>>(
47        error: E,
48        kind: Kind,
49        advice: &'static [&'static str],
50    ) -> Self {
51        Self {
52            error: error.into(),
53            kind,
54            advice,
55        }
56    }
57
58    /// Checks if this error is of a specific kind.
59    ///
60    /// Returns `true` if this error matches the provided [Kind],
61    /// otherwise `false`.
62    ///
63    /// # Examples
64    /// ```
65    /// use human_errors;
66    ///
67    /// let err = human_errors::user(
68    ///   "We could not open the config file you provided.",
69    ///   &["Make sure that the file exists and is readable by the application."],
70    /// );
71    ///
72    /// // Prints "is_user?: true"
73    /// println!("is_user?: {}", err.is(human_errors::Kind::User));
74    /// ```
75    pub fn is(&self, kind: Kind) -> bool {
76        self.kind == kind
77    }
78
79    /// Gets the description message from this error.
80    ///
81    /// Gets the description which was provided as the first argument when constructing
82    /// this error.
83    ///
84    /// # Examples
85    /// ```
86    /// use human_errors;
87    ///
88    /// let err = human_errors::user(
89    ///   "We could not open the config file you provided.",
90    ///   &["Make sure that the file exists and is readable by the application."],
91    /// );
92    ///
93    /// // Prints: "We could not open the config file you provided."
94    /// println!("{}", err.description())
95    /// ```
96    pub fn description(&self) -> String {
97        match self.error.downcast_ref::<Error>() {
98            Some(err) => err.description(),
99            None => format!("{}", self.error),
100        }
101    }
102
103    /// Gets the advice associated with this error and its causes.
104    ///
105    /// Gathers all advice from this error and any causal errors it wraps,
106    /// returning a deduplicated list of suggestions for how a user should
107    /// deal with this error.
108    ///
109    /// # Examples
110    /// ```
111    /// use human_errors;
112    ///
113    /// let err = human_errors::wrap_user(
114    ///   human_errors::user(
115    ///     "We could not find a file at /home/user/.config/demo.yml",
116    ///     &["Make sure that the file exists and is readable by the application."]
117    ///   ),
118    ///   "We could not open the config file you provided.",
119    ///   &["Make sure that you've specified a valid config file with the --config option."],
120    /// );
121    ///
122    /// // Prints:
123    /// // - Make sure that the file exists and is readable by the application.
124    /// // - Make sure that you've specified a valid config file with the --config option.
125    /// for tip in err.advice() {
126    ///     println!("- {}", tip);
127    /// }
128    /// ``````
129    pub fn advice(&self) -> Vec<&'static str> {
130        let mut advice = self.advice.to_vec();
131
132        let mut cause: Option<&(dyn std::error::Error + 'static)> = Some(self.error.as_ref());
133        while let Some(err) = cause {
134            if let Some(err) = err.downcast_ref::<Error>() {
135                advice.extend_from_slice(err.advice);
136            }
137
138            cause = err.source();
139        }
140
141        advice.reverse();
142
143        let mut seen = std::collections::HashSet::new();
144        advice.retain(|item| seen.insert(*item));
145
146        advice
147    }
148
149    /// Gets the formatted error and its advice.
150    ///
151    /// Generates a string containing the description of the error and any causes,
152    /// as well as a list of suggestions for how a user should
153    /// deal with this error. The "deepest" error's advice is presented first, with
154    /// successively higher errors appearing lower in the list. This is done because
155    /// the most specific error is the one most likely to have the best advice on how
156    /// to resolve the problem.
157    ///
158    /// # Examples
159    /// ```
160    /// use human_errors;
161    ///
162    /// let err = human_errors::wrap_user(
163    ///   human_errors::user(
164    ///     "We could not find a file at /home/user/.config/demo.yml",
165    ///     &["Make sure that the file exists and is readable by the application."]
166    ///   ),
167    ///   "We could not open the config file you provided.",
168    ///   &["Make sure that you've specified a valid config file with the --config option."],
169    /// );
170    ///
171    /// // Prints a message like the following:
172    /// // We could not open the config file you provided. (User error)
173    /// //
174    /// // This was caused by:
175    /// // We could not find a file at /home/user/.config/demo.yml
176    /// //
177    /// // To try and fix this, you can:
178    /// //  - Make sure that the file exists and is readable by the application.
179    /// //  - Make sure that you've specified a valid config file with the --config option.
180    /// println!("{}", err.message());
181    /// ```
182    pub fn message(&self) -> String {
183        let description = self.description();
184        let hero_message = self.kind.format_description(&description);
185
186        match (self.caused_by(), self.advice()) {
187            (cause, advice) if !cause.is_empty() && !advice.is_empty() => {
188                format!(
189                    "{}\n\nThis was caused by:\n - {}\n\nTo try and fix this, you can:\n - {}",
190                    hero_message,
191                    cause.join("\n - "),
192                    advice.join("\n - ")
193                )
194            }
195            (cause, _) if !cause.is_empty() => {
196                format!(
197                    "{}\n\nThis was caused by:\n - {}",
198                    hero_message,
199                    cause.join("\n - ")
200                )
201            }
202            (_, advice) if !advice.is_empty() => {
203                format!(
204                    "{}\n\nTo try and fix this, you can:\n - {}",
205                    hero_message,
206                    advice.join("\n - ")
207                )
208            }
209            _ => hero_message,
210        }
211    }
212
213    fn caused_by(&self) -> Vec<String> {
214        let mut causes = Vec::new();
215        let mut current_error: &dyn error::Error = self.error.as_ref();
216        while let Some(err) = current_error.source() {
217            if let Some(err) = err.downcast_ref::<Error>() {
218                causes.push(err.description());
219            } else {
220                causes.push(format!("{}", err));
221            }
222
223            current_error = err;
224        }
225
226        causes
227    }
228}
229
230impl error::Error for Error {
231    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
232        self.error.source()
233    }
234}
235
236impl fmt::Display for Error {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(f, "{}", self.message())
239    }
240}
241
242#[cfg(feature = "serde")]
243impl serde::Serialize for Error {
244    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
245    where
246        S: serde::Serializer,
247    {
248        let mut state = serializer.serialize_struct("Error", 3)?;
249        state.serialize_field("kind", &self.kind)?;
250        state.serialize_field("description", &self.description())?;
251        state.serialize_field("advice", &self.advice())?;
252        state.end()
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_basic_user_error() {
262        let err = Error::new(
263            "Something bad happened.",
264            Kind::User,
265            &["Avoid bad things happening in future"],
266        );
267
268        assert!(err.is(Kind::User));
269        assert_eq!(err.description(), "Something bad happened.");
270        assert_eq!(
271            err.message(),
272            "Something bad happened. (User error)\n\nTo try and fix this, you can:\n - Avoid bad things happening in future"
273        );
274    }
275
276    #[test]
277    fn test_basic_system_error() {
278        let err = Error::new(
279            "Something bad happened.",
280            Kind::System,
281            &["Avoid bad things happening in future"],
282        );
283
284        assert!(err.is(Kind::System));
285        assert_eq!(err.description(), "Something bad happened.");
286        assert_eq!(
287            err.message(),
288            "Something bad happened. (System failure)\n\nTo try and fix this, you can:\n - Avoid bad things happening in future"
289        );
290    }
291
292    #[test]
293    fn test_advice_aggregation() {
294        let low_level_err = Error::new(
295            "Low-level failure.",
296            Kind::System,
297            &["Check low-level systems"],
298        );
299
300        let high_level_err = Error::new(
301            low_level_err,
302            Kind::User,
303            &["Check high-level configuration"],
304        );
305
306        assert_eq!(
307            high_level_err.advice(),
308            vec!["Check low-level systems", "Check high-level configuration"]
309        );
310    }
311}