torrust_tracker_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
36pub type DynError = Arc<dyn std::error::Error + Send + Sync>;
37
38/// A generic wrapper around an error.
39///
40/// Where `E` is the inner error (source error).
41pub struct Located<E>(pub E);
42
43/// A wrapper around an error that includes the location of the error.
44#[derive(Debug)]
45pub struct LocatedError<'a, E>
46where
47    E: Error + ?Sized + Send + Sync,
48{
49    source: Arc<E>,
50    location: Box<Location<'a>>,
51}
52
53impl<'a, E> std::fmt::Display for LocatedError<'a, E>
54where
55    E: Error + ?Sized + Send + Sync,
56{
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        write!(f, "{}, {}", self.source, self.location)
59    }
60}
61
62impl<'a, E> Error for LocatedError<'a, E>
63where
64    E: Error + ?Sized + Send + Sync + 'static,
65{
66    fn source(&self) -> Option<&(dyn Error + 'static)> {
67        Some(&self.source)
68    }
69}
70
71impl<'a, E> Clone for LocatedError<'a, E>
72where
73    E: Error + ?Sized + Send + Sync,
74{
75    fn clone(&self) -> Self {
76        LocatedError {
77            source: self.source.clone(),
78            location: self.location.clone(),
79        }
80    }
81}
82
83#[allow(clippy::from_over_into)]
84impl<'a, E> Into<LocatedError<'a, E>> for Located<E>
85where
86    E: Error + Send + Sync,
87    Arc<E>: Clone,
88{
89    #[track_caller]
90    fn into(self) -> LocatedError<'a, E> {
91        let e = LocatedError {
92            source: Arc::new(self.0),
93            location: Box::new(*std::panic::Location::caller()),
94        };
95        tracing::debug!("{e}");
96        e
97    }
98}
99
100#[allow(clippy::from_over_into)]
101impl<'a> Into<LocatedError<'a, dyn std::error::Error + Send + Sync>> for DynError {
102    #[track_caller]
103    fn into(self) -> LocatedError<'a, dyn std::error::Error + Send + Sync> {
104        LocatedError {
105            source: self,
106            location: Box::new(*std::panic::Location::caller()),
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::panic::Location;
114
115    use super::LocatedError;
116    use crate::Located;
117
118    #[derive(thiserror::Error, Debug)]
119    enum TestError {
120        #[error("Test")]
121        Test,
122    }
123
124    #[track_caller]
125    fn get_caller_location() -> Location<'static> {
126        *Location::caller()
127    }
128
129    #[test]
130    fn error_should_include_location() {
131        let e = TestError::Test;
132
133        let b: LocatedError<'_, TestError> = Located(e).into();
134        let l = get_caller_location();
135
136        assert_eq!(b.location.file(), l.file());
137    }
138}