torrust_index_located_error/
lib.rs

1//! This crate provides a wrapper around an error that includes the location of
2//! the error.
3//!
4//! ```rust
5//! use std::error::Error;
6//! use std::panic::Location;
7//! use std::sync::Arc;
8//! use torrust_tracker_located_error::{Located, LocatedError};
9//!
10//! #[derive(thiserror::Error, Debug)]
11//! enum TestError {
12//!     #[error("Test")]
13//!     Test,
14//! }
15//!
16//! #[track_caller]
17//! fn get_caller_location() -> Location<'static> {
18//!     *Location::caller()
19//! }
20//!
21//! let e = TestError::Test;
22//!
23//! let b: LocatedError<TestError> = Located(e).into();
24//! let l = get_caller_location();
25//!
26//! assert!(b.to_string().contains("Test, src/lib.rs"));
27//! ```
28//!
29//! # Credits
30//!
31//! <https://stackoverflow.com/questions/74336993/getting-line-numbers-with-when-using-boxdyn-stderrorerror>
32use std::error::Error;
33use std::panic::Location;
34use std::sync::Arc;
35
36/// A generic wrapper around an error.
37///
38/// Where `E` is the inner error (source error).
39pub struct Located<E>(pub E);
40
41/// A wrapper around an error that includes the location of the error.
42#[derive(Debug)]
43pub struct LocatedError<'a, E>
44where
45    E: Error + ?Sized + Send + Sync,
46{
47    source: Arc<E>,
48    location: Box<Location<'a>>,
49}
50
51impl<'a, E> std::fmt::Display for LocatedError<'a, E>
52where
53    E: Error + ?Sized + Send + Sync,
54{
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        write!(f, "{}, {}", self.source, self.location)
57    }
58}
59
60impl<'a, E> Error for LocatedError<'a, E>
61where
62    E: Error + ?Sized + Send + Sync + 'static,
63{
64    fn source(&self) -> Option<&(dyn Error + 'static)> {
65        Some(&self.source)
66    }
67}
68
69impl<'a, E> Clone for LocatedError<'a, E>
70where
71    E: Error + ?Sized + Send + Sync,
72{
73    fn clone(&self) -> Self {
74        LocatedError {
75            source: self.source.clone(),
76            location: self.location.clone(),
77        }
78    }
79}
80
81#[allow(clippy::from_over_into)]
82impl<'a, E> Into<LocatedError<'a, E>> for Located<E>
83where
84    E: Error + Send + Sync,
85    Arc<E>: Clone,
86{
87    #[track_caller]
88    fn into(self) -> LocatedError<'a, E> {
89        let e = LocatedError {
90            source: Arc::new(self.0),
91            location: Box::new(*std::panic::Location::caller()),
92        };
93        tracing::debug!("{e}");
94        e
95    }
96}
97
98#[allow(clippy::from_over_into)]
99impl<'a> Into<LocatedError<'a, dyn std::error::Error + Send + Sync>> for Arc<dyn std::error::Error + Send + Sync> {
100    #[track_caller]
101    fn into(self) -> LocatedError<'a, dyn std::error::Error + Send + Sync> {
102        LocatedError {
103            source: self,
104            location: Box::new(*std::panic::Location::caller()),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use std::panic::Location;
112
113    use super::LocatedError;
114    use crate::Located;
115
116    #[derive(thiserror::Error, Debug)]
117    enum TestError {
118        #[error("Test")]
119        Test,
120    }
121
122    #[track_caller]
123    fn get_caller_location() -> Location<'static> {
124        *Location::caller()
125    }
126
127    #[test]
128    fn error_should_include_location() {
129        let e = TestError::Test;
130
131        let b: LocatedError<'_, TestError> = Located(e).into();
132        let l = get_caller_location();
133
134        assert_eq!(b.location.file(), l.file());
135    }
136}