app_error/
pretty.rs

1//! Error pretty printing
2
3// Imports
4use {
5	crate::{AppError, Inner},
6	core::fmt,
7	itertools::{Itertools, Position as ItertoolsPos},
8	std::vec,
9};
10
11/// Pretty display for [`AppError`]
12pub struct PrettyDisplay<'a, D = ()> {
13	/// Root error
14	root: &'a Inner<D>,
15
16	/// Ignore error
17	// TODO: Make this a closure?
18	ignore_err: Option<fn(&D) -> bool>,
19}
20
21impl<D> fmt::Debug for PrettyDisplay<'_, D>
22where
23	D: fmt::Debug + 'static,
24{
25	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26		match f.alternate() {
27			true => f
28				.debug_struct("PrettyDisplay")
29				.field_with("root", |f| write!(f, "{:#?}", self.root))
30				.field("ignore_err", &self.ignore_err.as_ref().map(|_| ()))
31				.finish(),
32			false => write!(f, "{self}"),
33		}
34	}
35}
36
37#[derive(PartialEq, Clone, Copy, Debug)]
38enum Column {
39	Line,
40	Empty,
41}
42
43impl Column {
44	/// Returns the string for this column
45	const fn as_str(self) -> &'static str {
46		match self {
47			Self::Line => "│ ",
48			Self::Empty => "  ",
49		}
50	}
51}
52
53impl<'a, D> PrettyDisplay<'a, D> {
54	/// Creates a new pretty display
55	pub(crate) fn new(root: &'a Inner<D>) -> Self {
56		Self { root, ignore_err: None }
57	}
58
59	/// Adds a callback that chooses whether to ignore an error
60	#[must_use]
61	pub fn with_ignore_err(self, ignore_err: fn(&D) -> bool) -> Self {
62		Self {
63			ignore_err: Some(ignore_err),
64			..self
65		}
66	}
67
68	/// Formats the message of an error
69	#[expect(clippy::unused_self, reason = "We might use it in the future")]
70	fn fmt_msg(&self, f: &mut fmt::Formatter<'_>, msg: &str, columns: &[Column]) -> fmt::Result {
71		for (msg_pos, msg) in msg.split_inclusive('\n').with_position() {
72			// If we already had a newline, then we need to pad all the columns again
73			if matches!(msg_pos, ItertoolsPos::Middle | ItertoolsPos::Last) {
74				for c in columns {
75					f.pad(c.as_str())?;
76				}
77			}
78
79			write!(f, "{msg}")?;
80		}
81
82		Ok(())
83	}
84
85	/// Formats a single error
86	fn fmt_single(&self, f: &mut fmt::Formatter<'_>, err: &Inner<D>, columns: &mut Vec<Column>) -> fmt::Result {
87		// If it's multiple, display it as multiple
88		let (msg, source) = match err {
89			Inner::Single { msg, source, .. } => (msg, source),
90			Inner::Multiple(errs) => return self.fmt_multiple(f, errs, columns),
91		};
92
93		// Else write the top-level error
94		self.fmt_msg(f, msg, columns)?;
95
96		// Then, if there's a cause, write the rest
97		if let Some(mut cur_source) = source.as_ref() {
98			let starting_columns = columns.len();
99			loop {
100				// Print the pre-amble
101				f.pad("\n")?;
102				for c in &*columns {
103					f.pad(c.as_str())?;
104				}
105				f.pad("└─")?;
106				columns.push(Column::Empty);
107
108				// Then check if we got to a multiple.
109				match &*cur_source.inner {
110					Inner::Single { msg, source, .. } => {
111						self.fmt_msg(f, msg, columns)?;
112
113						// And descend
114						cur_source = match source {
115							Some(source) => source,
116							_ => break,
117						};
118					},
119					Inner::Multiple(errs) => {
120						self.fmt_multiple(f, errs, columns)?;
121						break;
122					},
123				}
124			}
125			let _: vec::Drain<'_, _> = columns.drain(starting_columns..);
126		}
127
128		Ok(())
129	}
130
131	/// Formats multiple errors
132	fn fmt_multiple(&self, f: &mut fmt::Formatter<'_>, errs: &[AppError<D>], columns: &mut Vec<Column>) -> fmt::Result {
133		// Write the top-level error
134		// TODO: Allow customizing this.
135		write!(f, "Multiple errors:")?;
136
137		// For each error, write it
138		let mut ignored_errs = 0;
139		for (pos, err) in errs.iter().with_position() {
140			// If we should ignore the error, skip
141			if let Some(ignore_err) = self.ignore_err &&
142				self::should_ignore(&err.inner, ignore_err)
143			{
144				ignored_errs += 1;
145				continue;
146			}
147
148			f.pad("\n")?;
149			for c in &*columns {
150				f.pad(c.as_str())?;
151			}
152
153			// Note: We'll only print `└─` if we have no ignored errors, since if we do,
154			//       we need that to print the final line showcasing how many we ignored
155			match ignored_errs == 0 && matches!(pos, ItertoolsPos::Last | ItertoolsPos::Only) {
156				true => {
157					f.pad("└─")?;
158					columns.push(Column::Empty);
159				},
160				false => {
161					f.pad("├─")?;
162					columns.push(Column::Line);
163				},
164			}
165
166			self.fmt_single(f, &err.inner, columns)?;
167			let _: Option<_> = columns.pop();
168		}
169
170		if ignored_errs != 0 {
171			f.pad("\n")?;
172			for c in &*columns {
173				f.pad(c.as_str())?;
174			}
175			f.pad("└─")?;
176			write!(f, "({ignored_errs} ignored errors)")?;
177		}
178
179		Ok(())
180	}
181}
182
183impl<D> fmt::Display for PrettyDisplay<'_, D> {
184	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185		let mut columns = vec![];
186		self.fmt_single(f, self.root, &mut columns)?;
187		assert_eq!(columns.len(), 0, "There should be no columns after formatting");
188
189		Ok(())
190	}
191}
192
193// Returns whether an error should be ignored
194fn should_ignore<D>(err: &Inner<D>, ignore_err: fn(&D) -> bool) -> bool {
195	match err {
196		// When dealing with a single error, we ignore if it any error in it's tree, including itself
197		// should be ignored.
198		Inner::Single { source, data, .. } =>
199			ignore_err(data) ||
200				source
201					.as_ref()
202					.is_some_and(|source| self::should_ignore(&source.inner, ignore_err)),
203
204		// For multiple errors, we only ignore it if all should be ignored.
205		Inner::Multiple(errs) => errs.iter().all(|err| self::should_ignore(&err.inner, ignore_err)),
206	}
207}