human_errors/error.rs
1use std::{error, fmt};
2
3/// The kind of error which occurred.
4///
5/// Distinguishes between errors which were the result of user actions
6/// and those which were the result of system failures. Conceptually
7/// similar to HTTP status codes in that 4xx errors are user-caused
8/// and 5xx errors are system-caused.
9#[derive(Debug, PartialEq, Eq)]
10pub enum Kind {
11 /// An error which was the result of actions that the user took.
12 ///
13 /// These errors are usually things which a user can easily resolve by
14 /// changing how they interact with the system. Advice should be used
15 /// to guide the user to the correct interaction paths and help them
16 /// self-mitigate without needing to open support tickets.
17 ///
18 /// These errors are usually generated with [`crate::user`], [`crate::user_with_cause`]
19 /// and [`crate::user_with_internal`].
20 User,
21
22 /// An error which was the result of the system failing rather than the user's actions.
23 ///
24 /// These kinds of issues are usually the result of the system entering
25 /// an unexpected state and/or violating an assumption on behalf of the
26 /// developer. Often these issues cannot be resolved by the user directly,
27 /// so the advice should guide them to the best way to raise a bug with you
28 /// and provide you with information to help them fix the issue.
29 ///
30 /// These errors are usually generated with [`crate::system`], [`crate::system_with_cause`]
31 /// and [`crate::system_with_internal`].
32 System,
33}
34
35impl Kind {
36 fn format_description(&self, description: &str) -> String {
37 match self {
38 Kind::User => format!("Oh no! {description}"),
39 Kind::System => format!("Whoops! {description} (This isn't your fault)"),
40 }
41 }
42}
43
44/// The fundamental error type used by this library.
45///
46/// An error type which encapsulates information about whether an error
47/// is the result of something the user did, or a system failure outside
48/// their control. These errors include a description of what occurred,
49/// advice on how to proceed and references to the causal chain which led
50/// to this failure.
51///
52/// # Examples
53/// ```
54/// use human_errors;
55///
56/// let err = human_errors::user(
57/// "We could not open the config file you provided.",
58/// &["Make sure that the file exists and is readable by the application."],
59/// );
60///
61/// // Prints the error and any advice for the user.
62/// println!("{}", err)
63/// ```
64#[derive(Debug)]
65pub struct Error {
66 kind: Kind,
67 error: Box<dyn error::Error + Send + Sync>,
68 advice: &'static [&'static str],
69}
70
71impl Error {
72 /// Constructs a new [Error].
73 ///
74 /// # Examples
75 /// ```
76 /// use human_errors;
77 /// let err = human_errors::Error::new(
78 /// "Low-level IO error details",
79 /// human_errors::Kind::System,
80 /// &["Try restarting the application", "If the problem persists, contact support"]
81 /// );
82 /// ```
83 pub fn new<E: Into<Box<dyn error::Error + Send + Sync>>>(
84 error: E,
85 kind: Kind,
86 advice: &'static [&'static str],
87 ) -> Self {
88 Self {
89 error: error.into(),
90 kind,
91 advice,
92 }
93 }
94
95 /// Gets the description message from this error.
96 ///
97 /// Gets the description which was provided as the first argument when constructing
98 /// this error.
99 ///
100 /// # Examples
101 /// ```
102 /// use human_errors;
103 ///
104 /// let err = human_errors::user(
105 /// "We could not open the config file you provided.",
106 /// &["Make sure that the file exists and is readable by the application."],
107 /// );
108 ///
109 /// // Prints: "We could not open the config file you provided."
110 /// println!("{}", err.description())
111 /// ```
112 pub fn description(&self) -> String {
113 match self.error.downcast_ref::<Error>() {
114 Some(err) => err.description(),
115 None => format!("{}", self.error),
116 }
117 }
118
119 /// Gets the formatted error and its advice.
120 ///
121 /// Generates a string containing the description of the error and any causes,
122 /// as well as a list of suggestions for how a user should
123 /// deal with this error. The "deepest" error's advice is presented first, with
124 /// successively higher errors appearing lower in the list. This is done because
125 /// the most specific error is the one most likely to have the best advice on how
126 /// to resolve the problem.
127 ///
128 /// # Examples
129 /// ```
130 /// use human_errors;
131 ///
132 /// let err = human_errors::wrap_user(
133 /// human_errors::user(
134 /// "We could not find a file at /home/user/.config/demo.yml",
135 /// &["Make sure that the file exists and is readable by the application."]
136 /// ),
137 /// "We could not open the config file you provided.",
138 /// &["Make sure that you've specified a valid config file with the --config option."],
139 /// );
140 ///
141 /// // Prints a message like the following:
142 /// // Oh no! We could not open the config file you provided.
143 /// //
144 /// // This was caused by:
145 /// // We could not find a file at /home/user/.config/demo.yml
146 /// //
147 /// // To try and fix this, you can:
148 /// // - Make sure that the file exists and is readable by the application.
149 /// // - Make sure that you've specified a valid config file with the --config option.
150 /// println!("{}", err.message());
151 /// ```
152 pub fn message(&self) -> String {
153 let description = self.description();
154 let hero_message = self.kind.format_description(&description);
155
156 match (self.caused_by(), self.advice()) {
157 (cause, advice) if !cause.is_empty() && !advice.is_empty() => {
158 format!(
159 "{}\n\nThis was caused by:\n - {}\n\nTo try and fix this, you can:\n - {}",
160 hero_message,
161 cause.join("\n - "),
162 advice.join("\n - ")
163 )
164 }
165 (cause, _) if !cause.is_empty() => {
166 format!(
167 "{}\n\nThis was caused by:\n - {}",
168 hero_message,
169 cause.join("\n - ")
170 )
171 }
172 (_, advice) if !advice.is_empty() => {
173 format!(
174 "{}\n\nTo try and fix this, you can:\n - {}",
175 hero_message,
176 advice.join("\n - ")
177 )
178 }
179 _ => hero_message,
180 }
181 }
182
183 fn caused_by(&self) -> Vec<String> {
184 let mut causes = Vec::new();
185 let mut current_error: &dyn error::Error = self.error.as_ref();
186 while let Some(err) = current_error.source() {
187 if let Some(err) = err.downcast_ref::<Error>() {
188 causes.push(err.description());
189 } else {
190 causes.push(format!("{}", err));
191 }
192
193 current_error = err;
194 }
195
196 causes
197 }
198
199 fn advice(&self) -> Vec<&'static str> {
200 let mut advice = self.advice.to_vec();
201
202 let mut cause = self.error.as_ref();
203 while let Some(err) = cause.downcast_ref::<Error>() {
204 advice.extend_from_slice(err.advice);
205 cause = err.error.as_ref();
206 }
207
208 advice.reverse();
209
210 advice
211 }
212
213 /// Checks if this error is of a specific kind.
214 ///
215 /// Returns `true` if this error matches the provided [Kind],
216 /// otherwise `false`.
217 ///
218 /// # Examples
219 /// ```
220 /// use human_errors;
221 ///
222 /// let err = human_errors::user(
223 /// "We could not open the config file you provided.",
224 /// &["Make sure that the file exists and is readable by the application."],
225 /// );
226 ///
227 /// // Prints "is_user?: true"
228 /// println!("is_user?: {}", err.is(human_errors::Kind::User));
229 /// ```
230 pub fn is(&self, kind: Kind) -> bool {
231 self.kind == kind
232 }
233}
234
235impl error::Error for Error {
236 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
237 self.error.source()
238 }
239}
240
241impl fmt::Display for Error {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(f, "{}", self.message())
244 }
245}