use std::error::Error;
use std::fmt;
use std::sync::OnceLock;
#[cfg(any(feature = "anyhow", feature = "eyre"))]
use std::sync::Mutex;
#[cfg(feature = "macros")]
pub use error_path_macros::{error_path, error_path_impl, error_path_skip};
static PATH_DELIMITER: OnceLock<String> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SetPathDelimiterError {
Empty,
AlreadySet,
}
impl fmt::Display for SetPathDelimiterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => f.write_str("path delimiter must not be empty"),
Self::AlreadySet => f.write_str("path delimiter is already set"),
}
}
}
impl Error for SetPathDelimiterError {}
pub fn set_path_delimiter(delimiter: impl Into<String>) -> Result<(), SetPathDelimiterError> {
let delimiter = delimiter.into();
if delimiter.is_empty() {
return Err(SetPathDelimiterError::Empty);
}
PATH_DELIMITER
.set(delimiter)
.map_err(|_| SetPathDelimiterError::AlreadySet)
}
pub fn path_delimiter() -> &'static str {
PATH_DELIMITER.get_or_init(|| ".".to_owned()).as_str()
}
pub trait WithErrorPath: Sized {
fn with_error_path(self, path: &'static str) -> Self;
}
pub trait ErrorPathExt {
fn error_path(&self) -> Option<ErrorPath> {
None
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ErrorPath {
segments: Vec<&'static str>,
}
impl ErrorPath {
pub fn new() -> Self {
Self::default()
}
pub fn from_path(path: &'static str) -> Self {
Self {
segments: path
.split(path_delimiter())
.filter(|segment| !segment.is_empty())
.collect(),
}
}
pub fn from_dot_separated(path: &'static str) -> Self {
Self::from_path(path)
}
pub fn from_segment(segment: &'static str) -> Self {
let mut path = Self::new();
path.push(segment);
path
}
pub fn segments(&self) -> &[&'static str] {
&self.segments
}
pub fn to_string_with(&self, delimiter: &str) -> String {
self.segments.join(delimiter)
}
pub fn prepend_path(&mut self, path: &'static str) {
let prefix = Self::from_path(path);
self.segments.splice(0..0, prefix.segments);
}
pub fn prepend_dot_separated(&mut self, path: &'static str) {
self.prepend_path(path);
}
pub fn prepend(&mut self, segment: &'static str) {
if !segment.is_empty() {
self.segments.insert(0, segment);
}
}
pub fn push(&mut self, segment: &'static str) {
if !segment.is_empty() {
self.segments.push(segment);
}
}
}
impl fmt::Display for ErrorPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string_with(path_delimiter()))
}
}
#[cfg(any(feature = "anyhow", feature = "eyre"))]
#[derive(Debug)]
struct AdapterErrorPath(Mutex<ErrorPath>);
#[cfg(any(feature = "anyhow", feature = "eyre"))]
impl AdapterErrorPath {
fn new(path: &'static str) -> Self {
Self(Mutex::new(ErrorPath::from_path(path)))
}
fn prepend(&self, path: &'static str) {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.prepend_path(path);
}
fn path(&self) -> ErrorPath {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.clone()
}
}
#[cfg(any(feature = "anyhow", feature = "eyre"))]
impl fmt::Display for AdapterErrorPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.path().fmt(f)
}
}
#[cfg(feature = "anyhow")]
impl WithErrorPath for anyhow::Error {
fn with_error_path(self, path: &'static str) -> Self {
if let Some(error_path) = self.downcast_ref::<AdapterErrorPath>() {
error_path.prepend(path);
self
} else {
self.context(AdapterErrorPath::new(path))
}
}
}
#[cfg(feature = "anyhow")]
impl ErrorPathExt for anyhow::Error {
fn error_path(&self) -> Option<ErrorPath> {
self.downcast_ref::<AdapterErrorPath>()
.map(AdapterErrorPath::path)
}
}
#[cfg(feature = "eyre")]
impl WithErrorPath for eyre::Report {
fn with_error_path(self, path: &'static str) -> Self {
if let Some(error_path) = self.downcast_ref::<AdapterErrorPath>() {
error_path.prepend(path);
self
} else {
self.wrap_err(AdapterErrorPath::new(path))
}
}
}
#[cfg(feature = "eyre")]
impl ErrorPathExt for eyre::Report {
fn error_path(&self) -> Option<ErrorPath> {
self.downcast_ref::<AdapterErrorPath>()
.map(AdapterErrorPath::path)
}
}
#[cfg(feature = "error-stack")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ErrorPathSegment(pub &'static str);
#[cfg(feature = "error-stack")]
impl<C> WithErrorPath for error_stack::Report<C> {
fn with_error_path(self, path: &'static str) -> Self {
self.attach(ErrorPathSegment(path))
}
}
#[cfg(feature = "error-stack")]
impl<C> ErrorPathExt for error_stack::Report<C> {
fn error_path(&self) -> Option<ErrorPath> {
let segments: Vec<_> = self
.frames()
.filter_map(|frame| frame.downcast_ref::<ErrorPathSegment>())
.flat_map(|segment| {
segment
.0
.split(path_delimiter())
.filter(|part| !part.is_empty())
})
.collect();
(!segments.is_empty()).then_some(ErrorPath { segments })
}
}
#[cfg(feature = "stacked-errors")]
#[derive(Debug)]
struct StackedErrorPathSegment(&'static str);
#[cfg(feature = "stacked-errors")]
impl fmt::Display for StackedErrorPathSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
#[cfg(feature = "stacked-errors")]
impl Error for StackedErrorPathSegment {}
#[cfg(feature = "stacked-errors")]
impl WithErrorPath for stacked_errors::Error {
fn with_error_path(self, path: &'static str) -> Self {
self.add_kind_locationless(stacked_errors::ErrorKind::from_err(
StackedErrorPathSegment(path),
))
}
}
#[cfg(feature = "stacked-errors")]
impl ErrorPathExt for stacked_errors::Error {
fn error_path(&self) -> Option<ErrorPath> {
let segments: Vec<_> = self
.stack
.iter()
.rev()
.filter_map(|(kind, _)| match kind {
stacked_errors::ErrorKind::BoxedError(error) => {
error.downcast_ref::<StackedErrorPathSegment>()
}
_ => None,
})
.flat_map(|segment| {
segment
.0
.split(path_delimiter())
.filter(|part| !part.is_empty())
})
.collect();
(!segments.is_empty()).then_some(ErrorPath { segments })
}
}
#[cfg(all(
test,
any(feature = "anyhow", feature = "eyre", feature = "stacked-errors")
))]
mod tests {
use super::*;
#[cfg(feature = "anyhow")]
#[test]
fn anyhow_adapter_adds_path_context() {
let err = anyhow::anyhow!("root").with_error_path("service.load");
assert_eq!(err.to_string(), "service.load");
}
#[cfg(feature = "anyhow")]
#[test]
fn anyhow_adapter_exposes_only_macro_path() {
let err = anyhow::anyhow!("HTTP communication failed")
.with_error_path("request_login")
.with_error_path("login.api");
assert_eq!(
err.error_path().unwrap().to_string(),
"login.api.request_login"
);
}
#[cfg(feature = "eyre")]
#[test]
fn eyre_adapter_adds_path_context() {
let err = eyre::eyre!("root").with_error_path("service.load");
assert_eq!(err.to_string(), "service.load");
}
#[cfg(feature = "eyre")]
#[test]
fn eyre_adapter_exposes_only_macro_path() {
let err = eyre::eyre!("HTTP communication failed")
.with_error_path("request_login")
.with_error_path("login.api");
assert_eq!(
err.error_path().unwrap().to_string(),
"login.api.request_login"
);
}
#[cfg(feature = "error-stack")]
#[test]
fn error_stack_adapter_exposes_only_macro_path() {
let err = error_stack::Report::new(std::io::Error::other("HTTP communication failed"))
.with_error_path("request_login")
.with_error_path("login.api");
assert_eq!(
err.error_path().unwrap().to_string(),
"login.api.request_login"
);
}
#[cfg(feature = "stacked-errors")]
#[test]
fn stacked_errors_adapter_adds_path_context() {
let err =
stacked_errors::Error::from_kind_locationless("root").with_error_path("service.load");
let rendered = err.to_string();
assert!(rendered.contains("root"));
assert!(rendered.contains("service.load"));
}
#[cfg(feature = "stacked-errors")]
#[test]
fn stacked_errors_adapter_exposes_only_macro_path() {
let err = stacked_errors::Error::from_kind_locationless("HTTP communication failed")
.with_error_path("request_login")
.with_error_path("login.api");
assert_eq!(
err.error_path().unwrap().to_string(),
"login.api.request_login"
);
}
}