neuer_error/
error.rs

1//! Error type implementation.
2
3use ::alloc::{borrow::Cow, boxed::Box, vec, vec::Vec};
4use ::core::{
5	any::Any,
6	error::Error,
7	fmt::{Debug, Display, Formatter, Result as FmtResult},
8	panic::Location,
9};
10#[cfg(feature = "colors")]
11use ::yansi::Paint;
12
13use crate::features::{AnyDebugSendSync, ErrorSendSync};
14
15/// Error information for humans.
16/// Error message with location information.
17#[derive(Debug)]
18pub(crate) struct HumanInfo {
19	/// Message text.
20	pub(crate) message: Cow<'static, str>,
21	/// Location of occurrence.
22	pub(crate) location: &'static Location<'static>,
23}
24
25/// Error information for machines.
26/// Arbitrary, project specific types of information.
27#[derive(Debug)]
28pub(crate) struct MachineInfo {
29	/// Attachment.
30	pub(crate) attachment: Box<dyn AnyDebugSendSync>,
31}
32
33/// Context information, either machine or human.
34/// Joined in a union type to save the space of another `Vec` in the error type.
35#[derive(Debug)]
36pub(crate) enum Info {
37	/// Contextual information for humans.
38	Human(HumanInfo),
39	/// Contextual information for machines.
40	Machine(MachineInfo),
41}
42// Ensure niche-optimization is active.
43const _: () = {
44	assert!(size_of::<Info>() == size_of::<HumanInfo>());
45};
46
47/// Generic rich error type for use within `Result`s, for libraries and applications.
48///
49/// Add human context information, including code locations, via the `context` method.
50/// Attach machine context information via the `attach` and `attach_override` methods.
51///
52/// ## Error Formatting
53///
54/// The normal `Debug` implementation (`"{err:?}"`) will print the error with multi-line formatting,
55/// exactly how `Display` is doing it. The alternate `Debug` implementation (`"{err:#?}"`) will show
56/// the pretty-printed usual debug representation of the internal types.
57///
58/// When using the `Display` implementation, the normal implementation (`"{err}"`) will use
59/// multi-line formatting. You can use the alternate format (`{err:#}`) to get a compact single-line
60/// version. instead of multi-line formatted.
61#[derive(Default)]
62pub struct NeuErr(NeuErrImpl);
63
64/// Inner implementation of [`NeuErr`] that implements [`Error`].
65#[derive(Default)]
66pub struct NeuErrImpl {
67	/// Contextual error information.
68	infos: Vec<Info>,
69	/// Source error.
70	source: Option<Box<dyn ErrorSendSync>>,
71}
72
73impl Debug for NeuErr {
74	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
75		Debug::fmt(&self.0, f)
76	}
77}
78
79impl Display for NeuErr {
80	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
81		Display::fmt(&self.0, f)
82	}
83}
84
85impl Debug for NeuErrImpl {
86	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
87		if f.alternate() {
88			f.debug_struct("NeuErr")
89				.field("infos", &self.infos)
90				.field("source", &self.source)
91				.finish()
92		} else {
93			Display::fmt(self, f)
94		}
95	}
96}
97
98impl Display for NeuErrImpl {
99	fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
100		let mut human = self.contexts().peekable();
101		if human.peek().is_none() {
102			#[cfg(feature = "colors")]
103			let unknown = "Unknown error".red();
104			#[cfg(not(feature = "colors"))]
105			let unknown = "Unknown error";
106
107			write!(f, "{unknown}")?;
108		}
109		while let Some(context) = human.next() {
110			#[cfg(feature = "colors")]
111			let message = context.message.as_ref().red();
112			#[cfg(not(feature = "colors"))]
113			let message = context.message.as_ref();
114
115			#[cfg(feature = "colors")]
116			let location = context.location.rgb(0x90, 0x90, 0x90);
117			#[cfg(not(feature = "colors"))]
118			let location = context.location;
119
120			if f.alternate() {
121				write!(f, "{message} (at {location})")?;
122				if human.peek().is_some() {
123					write!(f, "; ")?;
124				}
125			} else {
126				writeln!(f, "{message}")?;
127				write!(f, "|- at {location}")?;
128				if human.peek().is_some() {
129					writeln!(f)?;
130					writeln!(f, "|")?;
131				}
132			}
133		}
134
135		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
136		let mut source = self.source.as_deref().map(|e| e as &(dyn Error + 'static));
137		while let Some(err) = source {
138			#[cfg(feature = "colors")]
139			let error = err.red();
140			#[cfg(not(feature = "colors"))]
141			let error = err;
142
143			if f.alternate() {
144				write!(f, "; caused by: {error}")?;
145			} else {
146				writeln!(f)?;
147				writeln!(f, "|")?;
148				write!(f, "|- caused by: {error}")?;
149			}
150
151			source = err.source();
152		}
153
154		Ok(())
155	}
156}
157
158impl NeuErr {
159	/// Create new error.
160	#[track_caller]
161	#[must_use]
162	#[inline]
163	pub fn new<C>(context: C) -> Self
164	where
165		C: Into<Cow<'static, str>>,
166	{
167		let infos =
168			vec![Info::Human(HumanInfo { message: context.into(), location: Location::caller() })];
169		Self(NeuErrImpl { infos, ..Default::default() })
170	}
171
172	/// Create new error from source error.
173	#[track_caller]
174	#[must_use]
175	#[inline]
176	pub fn new_with_source<C, E>(context: C, source: E) -> Self
177	where
178		C: Into<Cow<'static, str>>,
179		E: ErrorSendSync + 'static,
180	{
181		let infos =
182			vec![Info::Human(HumanInfo { message: context.into(), location: Location::caller() })];
183		Self(NeuErrImpl { infos, source: Some(Box::new(source)) })
184	}
185
186	/// Convert source error.
187	#[must_use]
188	#[inline]
189	pub fn from_source<E>(source: E) -> Self
190	where
191		E: ErrorSendSync + 'static,
192	{
193		Self(NeuErrImpl { source: Some(Box::new(source)), ..Default::default() })
194	}
195
196	/// Add human context to the error.
197	#[track_caller]
198	#[must_use]
199	#[inline]
200	pub fn context<C>(self, context: C) -> Self
201	where
202		C: Into<Cow<'static, str>>,
203	{
204		Self(self.0.context(context))
205	}
206
207	/// Add machine context to the error.
208	///
209	/// This will not override existing attachments. If you want to replace and override any
210	/// existing attachments of the same type, use `attach_override` instead.
211	#[must_use]
212	#[inline]
213	pub fn attach<C>(self, context: C) -> Self
214	where
215		C: AnyDebugSendSync + 'static,
216	{
217		Self(self.0.attach(context))
218	}
219
220	/// Set machine context in the error.
221	///
222	/// This will override existing attachments of the same type. If you want to add attachments of
223	/// the same type, use `attach` instead.
224	#[must_use]
225	pub fn attach_override<C>(self, context: C) -> Self
226	where
227		C: AnyDebugSendSync + 'static,
228	{
229		Self(self.0.attach_override(context))
230	}
231
232	/// Get an iterator over the human context infos.
233	#[inline]
234	#[cfg_attr(not(test), expect(unused, reason = "For consistency"))]
235	pub(crate) fn contexts(&self) -> impl Iterator<Item = &'_ HumanInfo> {
236		self.0.contexts()
237	}
238
239	/// Get an iterator over the machine context attachments of the given type.
240	#[inline]
241	pub fn attachments<C>(&self) -> impl Iterator<Item = &'_ C>
242	where
243		C: AnyDebugSendSync + 'static,
244	{
245		self.0.attachments()
246	}
247
248	/// Get the machine context attachment of the given type.
249	#[must_use]
250	#[inline]
251	pub fn attachment<C>(&self) -> Option<&C>
252	where
253		C: AnyDebugSendSync + 'static,
254	{
255		self.0.attachment()
256	}
257
258	/// Get the source error.
259	#[must_use]
260	#[inline]
261	pub fn source(&self) -> Option<&(dyn ErrorSendSync + 'static)> {
262		self.0.source.as_deref()
263	}
264
265	/// Unwrap this error into a [`NeuErrImpl`] that implements [`Error`]. Note however, that it
266	/// does not offer all of the functionality and might be unwieldy for other general purposes
267	/// than interfacing with other error types.
268	#[must_use]
269	#[inline]
270	pub fn into_error(self) -> NeuErrImpl {
271		self.0
272	}
273}
274
275impl NeuErrImpl {
276	/// Wrap this error back into a [`NeuErr`] that offers all of the functionality.
277	#[must_use]
278	#[inline]
279	pub const fn wrap(self) -> NeuErr {
280		NeuErr(self)
281	}
282
283	/// Add human context to the error.
284	#[track_caller]
285	#[must_use]
286	#[inline]
287	pub fn context<C>(mut self, context: C) -> Self
288	where
289		C: Into<Cow<'static, str>>,
290	{
291		let context = HumanInfo { message: context.into(), location: Location::caller() };
292		self.infos.push(Info::Human(context));
293		self
294	}
295
296	/// Add machine context to the error.
297	///
298	/// This will not override existing attachments. If you want to replace and override any
299	/// existing attachments of the same type, use `attach_override` instead.
300	#[must_use]
301	#[inline]
302	pub fn attach<C>(mut self, context: C) -> Self
303	where
304		C: AnyDebugSendSync + 'static,
305	{
306		let context = MachineInfo { attachment: Box::new(context) };
307		self.infos.push(Info::Machine(context));
308		self
309	}
310
311	/// Set machine context in the error.
312	///
313	/// This will override existing attachments of the same type. If you want to add attachments of
314	/// the same type, use `attach` instead.
315	#[must_use]
316	pub fn attach_override<C>(mut self, mut context: C) -> Self
317	where
318		C: AnyDebugSendSync + 'static,
319	{
320		let mut inserted = false;
321		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
322		self.infos.retain_mut(|info| match info {
323			Info::Machine(ctx) => {
324				if let Some(content) =
325					(ctx.attachment.as_mut() as &mut (dyn Any + 'static)).downcast_mut::<C>()
326				{
327					if !inserted {
328						core::mem::swap(content, &mut context);
329						inserted = true;
330						true // First attachment of same type, was replaced with new value, so keep it.
331					} else {
332						false // Another attachment of the same type, remove duplicate.
333					}
334				} else {
335					true // Attachment of different type.
336				}
337			}
338			_ => true,
339		});
340		if !inserted {
341			// No existing attachment of the same type was found to be replaced, so add a new one.
342			self.infos.push(Info::Machine(MachineInfo { attachment: Box::new(context) }));
343		}
344		self
345	}
346
347	/// Get an iterator over all context infos.
348	#[inline]
349	pub(crate) fn infos(&self) -> impl Iterator<Item = &'_ Info> {
350		self.infos.iter().rev()
351	}
352
353	/// Get an iterator over the human context infos.
354	#[inline]
355	pub(crate) fn contexts(&self) -> impl Iterator<Item = &'_ HumanInfo> {
356		self.infos().filter_map(|info| match info {
357			Info::Human(info) => Some(info),
358			_ => None,
359		})
360	}
361
362	/// Get an iterator over the machine context attachments of the given type.
363	#[inline]
364	pub fn attachments<C>(&self) -> impl Iterator<Item = &'_ C>
365	where
366		C: AnyDebugSendSync + 'static,
367	{
368		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
369		self.infos()
370			.filter_map(|info| match info {
371				Info::Machine(info) => Some(info),
372				_ => None,
373			}) // Catch the newest attachment first.
374			.map(|ctx| ctx.attachment.as_ref() as &(dyn Any + 'static))
375			.filter_map(|ctx| ctx.downcast_ref())
376	}
377
378	/// Get the machine context attachment of the given type.
379	#[must_use]
380	#[inline]
381	pub fn attachment<C>(&self) -> Option<&C>
382	where
383		C: AnyDebugSendSync + 'static,
384	{
385		self.attachments().next()
386	}
387}
388
389impl From<NeuErr> for NeuErrImpl {
390	#[inline]
391	fn from(err: NeuErr) -> Self {
392		err.0
393	}
394}
395
396#[diagnostic::do_not_recommend]
397impl<E> From<E> for NeuErr
398where
399	E: ErrorSendSync + 'static,
400{
401	#[inline]
402	fn from(err: E) -> Self {
403		Self::from_source(err)
404	}
405}
406
407impl Error for NeuErrImpl {
408	#[inline]
409	fn source(&self) -> Option<&(dyn Error + 'static)> {
410		#[expect(trivial_casts, reason = "Not that trivial as it seems? False positive")]
411		self.source.as_deref().map(|e| e as &(dyn Error + 'static))
412	}
413}
414
415impl AsRef<dyn Error> for NeuErr {
416	#[inline]
417	fn as_ref(&self) -> &(dyn Error + 'static) {
418		&self.0
419	}
420}
421
422#[cfg(feature = "send")]
423impl AsRef<dyn Error + Send> for NeuErr {
424	#[inline]
425	fn as_ref(&self) -> &(dyn Error + Send + 'static) {
426		&self.0
427	}
428}
429
430#[cfg(all(feature = "send", feature = "sync"))]
431impl AsRef<dyn Error + Send + Sync> for NeuErr {
432	#[inline]
433	fn as_ref(&self) -> &(dyn Error + Send + Sync + 'static) {
434		&self.0
435	}
436}
437
438impl From<NeuErr> for Box<dyn Error> {
439	#[inline]
440	fn from(this: NeuErr) -> Self {
441		Box::new(this.into_error())
442	}
443}
444
445#[cfg(feature = "send")]
446impl From<NeuErr> for Box<dyn Error + Send> {
447	#[inline]
448	fn from(this: NeuErr) -> Self {
449		Box::new(this.into_error())
450	}
451}
452
453#[cfg(all(feature = "send", feature = "sync"))]
454impl From<NeuErr> for Box<dyn Error + Send + Sync> {
455	#[inline]
456	fn from(this: NeuErr) -> Self {
457		Box::new(this.into_error())
458	}
459}
460
461
462#[cfg(feature = "std")]
463impl std::process::Termination for NeuErr {
464	#[inline]
465	fn report(self) -> std::process::ExitCode {
466		std::process::Termination::report(self.0)
467	}
468}
469
470#[cfg(feature = "std")]
471impl std::process::Termination for NeuErrImpl {
472	#[inline]
473	fn report(self) -> std::process::ExitCode {
474		self.attachment::<std::process::ExitCode>()
475			.copied()
476			.unwrap_or(std::process::ExitCode::FAILURE)
477	}
478}