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}