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}