Skip to content

Commit 8df7d66

Browse files
committed
feat: Add support for rendering a pretty error to the CLI
1 parent 9c1f84e commit 8df7d66

5 files changed

Lines changed: 341 additions & 21 deletions

File tree

Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@ edition = "2024"
1414
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1515

1616
[dependencies]
17+
cli-boxes = { version = "0.1", optional = true }
18+
colored = { version = "2.0", optional = true }
19+
serde = { version = "1.0", optional = true }
20+
textwrap = { version = "0.16.2", optional = true }
21+
22+
[features]
23+
default = []
24+
cli = ["dep:cli-boxes", "dep:colored", "dep:textwrap"]
25+
serde = ["dep:serde"]

src/error.rs

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::{error, fmt};
22
use super::Kind;
33

4+
#[cfg(feature = "serde")]
5+
use serde::ser::SerializeStruct;
6+
47
/// The fundamental error type used by this library.
58
///
69
/// An error type which encapsulates information about whether an error
@@ -23,9 +26,9 @@ use super::Kind;
2326
/// ```
2427
#[derive(Debug)]
2528
pub struct Error {
26-
kind: Kind,
27-
error: Box<dyn error::Error + Send + Sync>,
28-
advice: &'static [&'static str],
29+
pub(crate) kind: Kind,
30+
pub(crate) error: Box<dyn error::Error + Send + Sync>,
31+
pub(crate) advice: &'static [&'static str],
2932
}
3033

3134
impl Error {
@@ -97,6 +100,52 @@ impl Error {
97100
}
98101
}
99102

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+
100149
/// Gets the formatted error and its advice.
101150
///
102151
/// Generates a string containing the description of the error and any causes,
@@ -120,7 +169,7 @@ impl Error {
120169
/// );
121170
///
122171
/// // Prints a message like the following:
123-
/// // Oh no! We could not open the config file you provided.
172+
/// // We could not open the config file you provided. (User error)
124173
/// //
125174
/// // This was caused by:
126175
/// // We could not find a file at /home/user/.config/demo.yml
@@ -176,23 +225,6 @@ impl Error {
176225

177226
causes
178227
}
179-
180-
fn advice(&self) -> Vec<&'static str> {
181-
let mut advice = self.advice.to_vec();
182-
183-
let mut cause = self.error.as_ref();
184-
while let Some(err) = cause.downcast_ref::<Error>() {
185-
advice.extend_from_slice(err.advice);
186-
cause = err.error.as_ref();
187-
}
188-
189-
advice.reverse();
190-
191-
let mut seen = std::collections::HashSet::new();
192-
advice.retain(|item| seen.insert(*item));
193-
194-
advice
195-
}
196228
}
197229

198230
impl error::Error for Error {
@@ -207,6 +239,20 @@ impl fmt::Display for Error {
207239
}
208240
}
209241

242+
#[cfg(feature = "serde")]
243+
impl 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+
210256
#[cfg(test)]
211257
mod tests {
212258
use super::*;
@@ -242,4 +288,24 @@ mod tests {
242288
"Something bad happened. (System failure)\n\nTo try and fix this, you can:\n - Avoid bad things happening in future"
243289
);
244290
}
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+
}
245311
}

src/kind.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,17 @@ impl Kind {
3838
Kind::System => format!("{description} (System failure)"),
3939
}
4040
}
41+
}
42+
43+
#[cfg(feature = "serde")]
44+
impl serde::Serialize for Kind {
45+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46+
where
47+
S: serde::Serializer,
48+
{
49+
match self {
50+
Kind::User => serializer.serialize_str("User"),
51+
Kind::System => serializer.serialize_str("System"),
52+
}
53+
}
4154
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ mod error;
99
mod from;
1010
mod helpers;
1111
mod kind;
12+
mod renderer;
1213
mod result;
1314
mod wrapper;
1415

1516
pub use error::*;
1617
pub use helpers::*;
1718
pub use kind::*;
19+
pub use renderer::*;
1820
pub use result::ResultExt;
1921
pub use wrapper::*;

0 commit comments

Comments
 (0)