Skip to main content

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}