Skip to main content

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