error_forge/group_macro.rs
1/// Provides macros for grouping errors and generating automatic conversions
2// StdError used in generated code from macros
3#[allow(unused_imports)]
4use std::error::Error as StdError;
5
6/// Macro for composing multi-error enums with automatic `From<OtherError>` conversions.
7///
8/// This macro allows you to create a parent error type that can wrap multiple other error types,
9/// automatically implementing From conversions for each of them.
10///
11/// # Example
12///
13/// ```ignore
14/// use error_forge::{group, AppError};
15/// use std::io;
16///
17/// group! {
18/// #[derive(Debug)]
19/// pub enum ServiceError {
20/// App(AppError),
21/// Io(io::Error),
22/// }
23/// }
24/// ```
25///
26/// # Known limitations (scheduled for `1.0`)
27///
28/// 1. **Macro-parse ambiguity.** The doctest above is marked
29/// `ignore` because the macro's internal `@with_impl` arm has
30/// two competing repetition blocks (`$variant` for wrapped
31/// types and `$variant_extra` for free-form variants) that the
32/// parser cannot disambiguate cleanly. `group!` works in
33/// practice as exercised by `tests/`, but a top-level doctest
34/// invocation trips the ambiguity. The macro will be rewritten
35/// with unambiguous tokens in `1.0`.
36/// 2. **Broken `ForgeError` delegation.** The generated
37/// `ForgeError` impl tries to delegate `kind` / `status_code` /
38/// `is_retryable` to the wrapped variant's own `ForgeError`
39/// impl, but the type-erased downcast pattern used internally
40/// does not work as intended. In practice every wrapped variant
41/// gets the fallback values (`stringify!($variant)` for `kind`,
42/// `500` for `status_code`, `false` for `is_retryable`). The
43/// `Display`, `Error::source()`, and `From<T>` parts work
44/// correctly. The delegation will be rewritten in `1.0` to
45/// require `: ForgeError` on each wrapped type and call its
46/// trait methods directly.
47#[macro_export]
48macro_rules! group {
49 // First pattern - simple wrapped errors without extra variants
50 (
51 $(#[$meta:meta])*
52 $vis:vis enum $name:ident {
53 $(
54 $(#[$vmeta:meta])*
55 $variant:ident($source_type:ty)
56 ),* $(,)?
57 }
58 ) => {
59 group!(@with_impl
60 $(#[$meta])* $vis enum $name {
61 $(
62 $(#[$vmeta])*
63 $variant($source_type),
64 )*
65 }
66 $(
67 $variant $source_type
68 )*
69 )
70 };
71
72 // Internal implementation with all necessary impls
73 (@with_impl
74 $(#[$meta:meta])* $vis:vis enum $name:ident {
75 $(
76 $(#[$vmeta:meta])*
77 $variant:ident($source_type:ty),
78 )*
79 $(
80 $(#[$vmeta_extra:meta])*
81 $variant_extra:ident $({$($field:ident: $field_type:ty),*})?,
82 )*
83 }
84 $($impl_variant:ident $impl_type:ty)*
85 ) => {
86 $(#[$meta])*
87 $vis enum $name {
88 $(
89 $(#[$vmeta])*
90 $variant($source_type),
91 )*
92 $(
93 $(#[$vmeta_extra])*
94 $variant_extra $({$($field: $field_type),*})?,
95 )*
96 }
97
98 impl std::fmt::Display for $name {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 $(
102 Self::$variant(source) => write!(f, "{}", source),
103 )*
104 $(
105 Self::$variant_extra $({$($field),*})? => {
106 let error_name = stringify!($variant_extra);
107 write!(f, "{} error", error_name)
108 }
109 )*
110 }
111 }
112 }
113
114 impl std::error::Error for $name {
115 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
116 match self {
117 $(
118 Self::$variant(source) => Some(source as &(dyn std::error::Error + 'static)),
119 )*
120 _ => None,
121 }
122 }
123 }
124
125 $(
126 impl From<$source_type> for $name {
127 fn from(source: $source_type) -> Self {
128 Self::$variant(source)
129 }
130 }
131 )*
132
133 // Implement ForgeError trait for our grouped error enum
134 impl $crate::error::ForgeError for $name {
135 fn kind(&self) -> &'static str {
136 match self {
137 $(
138 Self::$variant(source) => {
139 if let Some(forge_err) = (source as &dyn std::any::Any)
140 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
141 {
142 return forge_err.kind();
143 }
144 stringify!($variant)
145 },
146 )*
147 $(
148 Self::$variant_extra $({$($field),*})? => stringify!($variant_extra),
149 )*
150 }
151 }
152
153 fn user_message(&self) -> String {
154 match self {
155 $(
156 Self::$variant(source) => {
157 if let Some(forge_err) = (source as &dyn std::any::Any)
158 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
159 {
160 return forge_err.user_message();
161 }
162 source.to_string()
163 },
164 )*
165 $(
166 Self::$variant_extra $({$($field),*})? => self.to_string(),
167 )*
168 }
169 }
170
171 fn caption(&self) -> &'static str {
172 match self {
173 $(
174 Self::$variant(source) => {
175 if let Some(forge_err) = (source as &dyn std::any::Any)
176 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
177 {
178 return forge_err.caption();
179 }
180 concat!(stringify!($variant), ": Error")
181 },
182 )*
183 $(
184 Self::$variant_extra $({$($field),*})? => {
185 concat!(stringify!($variant_extra), ": Error")
186 },
187 )*
188 }
189 }
190
191 fn is_retryable(&self) -> bool {
192 match self {
193 $(
194 Self::$variant(source) => {
195 if let Some(forge_err) = (source as &dyn std::any::Any)
196 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
197 {
198 return forge_err.is_retryable();
199 }
200 false
201 },
202 )*
203 _ => false,
204 }
205 }
206
207 fn status_code(&self) -> u16 {
208 match self {
209 $(
210 Self::$variant(source) => {
211 if let Some(forge_err) = (source as &dyn std::any::Any)
212 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
213 {
214 return forge_err.status_code();
215 }
216 500
217 },
218 )*
219 _ => 500,
220 }
221 }
222
223 fn exit_code(&self) -> i32 {
224 match self {
225 $(
226 Self::$variant(source) => {
227 if let Some(forge_err) = (source as &dyn std::any::Any)
228 .downcast_ref::<&(dyn $crate::error::ForgeError)>()
229 {
230 return forge_err.exit_code();
231 }
232 1
233 },
234 )*
235 _ => 1,
236 }
237 }
238 }
239 };
240}