Skip to main content

scoped_error/
macros.rs

1// Copyright (C) 2026 Kan-Ru Chen <kanru@kanru.info>
2//
3// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5//! Helper macros and utilities.
6//!
7//! This module provides the [`impl_context_error!`] macro, which generates
8//! boilerplate implementations for custom error types.
9
10/// Implement context-aware error handling for a custom type.
11///
12/// This macro generates a struct definition and all necessary trait
13/// implementations to make a type work with [`expect_error`](crate::expect_error).
14///
15/// # Generated Structure
16///
17/// The macro generates a struct with these fields:
18/// - `message: Cow<'static, str>` - The error message
19/// - `source: Option<Box<dyn Error + Send + Sync>>` - The underlying error
20/// - `location: Option<&'static Location>` - Where the error was created
21///
22/// # Usage
23///
24/// ## Basic Usage (crate-private visibility)
25///
26/// ```
27/// use scoped_error::impl_context_error;
28///
29/// impl_context_error!(MyError);
30/// // Generates: pub(crate) struct MyError { ... }
31/// ```
32///
33/// ## Custom Visibility
34///
35/// ```
36/// use scoped_error::impl_context_error;
37///
38/// impl_context_error!(pub MyError);
39/// // Generates: pub struct MyError { ... }
40/// ```
41///
42/// # Generated Implementations
43///
44/// The macro generates:
45/// - `#[derive(Debug)]`
46/// - `std::error::Error` (with `source()` method)
47/// - `std::fmt::Display` (includes location when available)
48/// - `From<(Cow<'static, str>, Frame)>` (for use with `expect_error`)
49#[macro_export]
50macro_rules! impl_context_error {
51    ($error_type:ident) => {
52        impl_context_error!(pub(crate) $error_type);
53    };
54    ($vis:vis $error_type:ident) => {
55        #[derive(Debug)]
56        $vis struct $error_type {
57            message: std::borrow::Cow<'static, str>,
58            source: std::option::Option<Box<dyn std::error::Error + std::marker::Send + std::marker::Sync + 'static>>,
59            location: std::option::Option<&'static std::panic::Location<'static>>,
60        }
61        impl std::error::Error for $error_type {
62            fn source(&self) -> std::option::Option<&(dyn std::error::Error + 'static)> {
63                self.source.as_deref().map(|s| s as _)
64            }
65        }
66        impl std::fmt::Display for $error_type {
67            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68                if let Some(loc) = self.location {
69                    write!(f, "{}, at {}", self.message, loc)
70                } else {
71                    f.write_str(&self.message)
72                }
73            }
74        }
75        impl $crate::WithContext for $error_type {
76            fn with_context(mut self, context: $crate::Frame) -> Self {
77                self.source = Some(context.source);
78                self.location = Some(context.location);
79                self
80            }
81            fn location(&self) -> std::option::Option<&'static std::panic::Location<'static>> {
82                self.location
83            }
84        }
85        impl
86            From<(
87                std::borrow::Cow<'static, str>,
88                $crate::Frame,
89            )> for $error_type
90        {
91            fn from(
92                value: (
93                    std::borrow::Cow<'static, str>,
94                    $crate::Frame,
95                ),
96            ) -> Self {
97                Self {
98                    message: value.0,
99                    source: Some(value.1.source),
100                    location: Some(value.1.location),
101                }
102            }
103        }
104    };
105}
106
107#[cfg(test)]
108mod tests {
109    use crate::ext::ErrorExt;
110
111    impl_context_error!(Error);
112
113    #[test]
114    fn impl_error() {
115        let errors = Error {
116            message: "Failed to do something".into(),
117            source: Some(
118                Error {
119                    message: "Failed due to internal error".into(),
120                    source: Some(std::io::Error::other("Failed to perform IO").into()),
121                    location: None,
122                }
123                .into(),
124            ),
125            location: None,
126        };
127        assert_eq!(
128            errors.report().to_string(),
129            "Failed to do something\n|-- Failed due to internal error\n`-- Failed to perform IO"
130        );
131    }
132}