1use std::error::Error;
20use std::fmt;
21use std::sync::OnceLock;
22
23#[cfg(any(feature = "anyhow", feature = "eyre"))]
24use std::sync::Mutex;
25
26#[cfg(feature = "macros")]
27pub use error_path_macros::{error_path, error_path_impl, error_path_skip};
28
29static PATH_DELIMITER: OnceLock<String> = OnceLock::new();
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum SetPathDelimiterError {
35 Empty,
37 AlreadySet,
39}
40
41impl fmt::Display for SetPathDelimiterError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::Empty => f.write_str("path delimiter must not be empty"),
45 Self::AlreadySet => f.write_str("path delimiter is already set"),
46 }
47 }
48}
49
50impl Error for SetPathDelimiterError {}
51
52pub fn set_path_delimiter(delimiter: impl Into<String>) -> Result<(), SetPathDelimiterError> {
58 let delimiter = delimiter.into();
59 if delimiter.is_empty() {
60 return Err(SetPathDelimiterError::Empty);
61 }
62
63 PATH_DELIMITER
64 .set(delimiter)
65 .map_err(|_| SetPathDelimiterError::AlreadySet)
66}
67
68pub fn path_delimiter() -> &'static str {
72 PATH_DELIMITER.get_or_init(|| ".".to_owned()).as_str()
73}
74
75pub trait WithErrorPath: Sized {
80 fn with_error_path(self, path: &'static str) -> Self;
82}
83
84pub trait ErrorPathExt {
91 fn error_path(&self) -> Option<ErrorPath> {
93 None
94 }
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Eq)]
102pub struct ErrorPath {
103 segments: Vec<&'static str>,
104}
105
106impl ErrorPath {
107 pub fn new() -> Self {
109 Self::default()
110 }
111
112 pub fn from_path(path: &'static str) -> Self {
115 Self {
116 segments: path
117 .split(path_delimiter())
118 .filter(|segment| !segment.is_empty())
119 .collect(),
120 }
121 }
122
123 pub fn from_dot_separated(path: &'static str) -> Self {
128 Self::from_path(path)
129 }
130
131 pub fn from_segment(segment: &'static str) -> Self {
136 let mut path = Self::new();
137 path.push(segment);
138 path
139 }
140
141 pub fn segments(&self) -> &[&'static str] {
143 &self.segments
144 }
145
146 pub fn to_string_with(&self, delimiter: &str) -> String {
148 self.segments.join(delimiter)
149 }
150
151 pub fn prepend_path(&mut self, path: &'static str) {
154 let prefix = Self::from_path(path);
155 self.segments.splice(0..0, prefix.segments);
156 }
157
158 pub fn prepend_dot_separated(&mut self, path: &'static str) {
163 self.prepend_path(path);
164 }
165
166 pub fn prepend(&mut self, segment: &'static str) {
170 if !segment.is_empty() {
171 self.segments.insert(0, segment);
172 }
173 }
174
175 pub fn push(&mut self, segment: &'static str) {
179 if !segment.is_empty() {
180 self.segments.push(segment);
181 }
182 }
183}
184
185impl fmt::Display for ErrorPath {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 f.write_str(&self.to_string_with(path_delimiter()))
188 }
189}
190
191#[cfg(any(feature = "anyhow", feature = "eyre"))]
196#[derive(Debug)]
197struct AdapterErrorPath(Mutex<ErrorPath>);
198
199#[cfg(any(feature = "anyhow", feature = "eyre"))]
200impl AdapterErrorPath {
201 fn new(path: &'static str) -> Self {
202 Self(Mutex::new(ErrorPath::from_path(path)))
203 }
204
205 fn prepend(&self, path: &'static str) {
206 self.0
207 .lock()
208 .unwrap_or_else(|poisoned| poisoned.into_inner())
209 .prepend_path(path);
210 }
211
212 fn path(&self) -> ErrorPath {
213 self.0
214 .lock()
215 .unwrap_or_else(|poisoned| poisoned.into_inner())
216 .clone()
217 }
218}
219
220#[cfg(any(feature = "anyhow", feature = "eyre"))]
221impl fmt::Display for AdapterErrorPath {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 self.path().fmt(f)
224 }
225}
226
227#[cfg(feature = "anyhow")]
228impl WithErrorPath for anyhow::Error {
229 fn with_error_path(self, path: &'static str) -> Self {
230 if let Some(error_path) = self.downcast_ref::<AdapterErrorPath>() {
231 error_path.prepend(path);
232 self
233 } else {
234 self.context(AdapterErrorPath::new(path))
235 }
236 }
237}
238
239#[cfg(feature = "anyhow")]
240impl ErrorPathExt for anyhow::Error {
241 fn error_path(&self) -> Option<ErrorPath> {
242 self.downcast_ref::<AdapterErrorPath>()
243 .map(AdapterErrorPath::path)
244 }
245}
246
247#[cfg(feature = "eyre")]
248impl WithErrorPath for eyre::Report {
249 fn with_error_path(self, path: &'static str) -> Self {
250 if let Some(error_path) = self.downcast_ref::<AdapterErrorPath>() {
251 error_path.prepend(path);
252 self
253 } else {
254 self.wrap_err(AdapterErrorPath::new(path))
255 }
256 }
257}
258
259#[cfg(feature = "eyre")]
260impl ErrorPathExt for eyre::Report {
261 fn error_path(&self) -> Option<ErrorPath> {
262 self.downcast_ref::<AdapterErrorPath>()
263 .map(AdapterErrorPath::path)
264 }
265}
266
267#[cfg(feature = "error-stack")]
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
270pub struct ErrorPathSegment(pub &'static str);
271
272#[cfg(feature = "error-stack")]
273impl<C> WithErrorPath for error_stack::Report<C> {
274 fn with_error_path(self, path: &'static str) -> Self {
275 self.attach(ErrorPathSegment(path))
276 }
277}
278
279#[cfg(feature = "error-stack")]
280impl<C> ErrorPathExt for error_stack::Report<C> {
281 fn error_path(&self) -> Option<ErrorPath> {
282 let segments: Vec<_> = self
283 .frames()
284 .filter_map(|frame| frame.downcast_ref::<ErrorPathSegment>())
285 .flat_map(|segment| {
286 segment
287 .0
288 .split(path_delimiter())
289 .filter(|part| !part.is_empty())
290 })
291 .collect();
292
293 (!segments.is_empty()).then_some(ErrorPath { segments })
294 }
295}
296
297#[cfg(feature = "stacked-errors")]
299#[derive(Debug)]
300struct StackedErrorPathSegment(&'static str);
301
302#[cfg(feature = "stacked-errors")]
303impl fmt::Display for StackedErrorPathSegment {
304 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305 f.write_str(self.0)
306 }
307}
308
309#[cfg(feature = "stacked-errors")]
310impl Error for StackedErrorPathSegment {}
311
312#[cfg(feature = "stacked-errors")]
313impl WithErrorPath for stacked_errors::Error {
314 fn with_error_path(self, path: &'static str) -> Self {
315 self.add_kind_locationless(stacked_errors::ErrorKind::from_err(
316 StackedErrorPathSegment(path),
317 ))
318 }
319}
320
321#[cfg(feature = "stacked-errors")]
322impl ErrorPathExt for stacked_errors::Error {
323 fn error_path(&self) -> Option<ErrorPath> {
324 let segments: Vec<_> = self
325 .stack
326 .iter()
327 .rev()
328 .filter_map(|(kind, _)| match kind {
329 stacked_errors::ErrorKind::BoxedError(error) => {
330 error.downcast_ref::<StackedErrorPathSegment>()
331 }
332 _ => None,
333 })
334 .flat_map(|segment| {
335 segment
336 .0
337 .split(path_delimiter())
338 .filter(|part| !part.is_empty())
339 })
340 .collect();
341
342 (!segments.is_empty()).then_some(ErrorPath { segments })
343 }
344}
345
346#[cfg(all(
347 test,
348 any(feature = "anyhow", feature = "eyre", feature = "stacked-errors")
349))]
350mod tests {
351 use super::*;
352
353 #[cfg(feature = "anyhow")]
354 #[test]
355 fn anyhow_adapter_adds_path_context() {
356 let err = anyhow::anyhow!("root").with_error_path("service.load");
357
358 assert_eq!(err.to_string(), "service.load");
359 }
360
361 #[cfg(feature = "anyhow")]
362 #[test]
363 fn anyhow_adapter_exposes_only_macro_path() {
364 let err = anyhow::anyhow!("HTTP communication failed")
365 .with_error_path("request_login")
366 .with_error_path("login.api");
367
368 assert_eq!(
369 err.error_path().unwrap().to_string(),
370 "login.api.request_login"
371 );
372 }
373
374 #[cfg(feature = "eyre")]
375 #[test]
376 fn eyre_adapter_adds_path_context() {
377 let err = eyre::eyre!("root").with_error_path("service.load");
378
379 assert_eq!(err.to_string(), "service.load");
380 }
381
382 #[cfg(feature = "eyre")]
383 #[test]
384 fn eyre_adapter_exposes_only_macro_path() {
385 let err = eyre::eyre!("HTTP communication failed")
386 .with_error_path("request_login")
387 .with_error_path("login.api");
388
389 assert_eq!(
390 err.error_path().unwrap().to_string(),
391 "login.api.request_login"
392 );
393 }
394
395 #[cfg(feature = "error-stack")]
396 #[test]
397 fn error_stack_adapter_exposes_only_macro_path() {
398 let err = error_stack::Report::new(std::io::Error::other("HTTP communication failed"))
399 .with_error_path("request_login")
400 .with_error_path("login.api");
401
402 assert_eq!(
403 err.error_path().unwrap().to_string(),
404 "login.api.request_login"
405 );
406 }
407
408 #[cfg(feature = "stacked-errors")]
409 #[test]
410 fn stacked_errors_adapter_adds_path_context() {
411 let err =
412 stacked_errors::Error::from_kind_locationless("root").with_error_path("service.load");
413
414 let rendered = err.to_string();
415 assert!(rendered.contains("root"));
416 assert!(rendered.contains("service.load"));
417 }
418
419 #[cfg(feature = "stacked-errors")]
420 #[test]
421 fn stacked_errors_adapter_exposes_only_macro_path() {
422 let err = stacked_errors::Error::from_kind_locationless("HTTP communication failed")
423 .with_error_path("request_login")
424 .with_error_path("login.api");
425
426 assert_eq!(
427 err.error_path().unwrap().to_string(),
428 "login.api.request_login"
429 );
430 }
431}