Skip to main content

error_forge/
group_macro.rs

1/// Provides macros for grouping errors and generating automatic
2/// conversions.
3// `StdError` is referenced from generated code in the macro body.
4#[allow(unused_imports)]
5use std::error::Error as StdError;
6
7/// Macro for composing multi-error enums with automatic
8/// `From<OtherError>` conversions and full [`ForgeError`] delegation.
9///
10/// Every variant must wrap exactly one source type that itself
11/// implements [`ForgeError`]. The macro generates:
12///
13/// - the enum declaration,
14/// - `Display` and `Error` implementations that forward to the
15///   wrapped source,
16/// - `From<T>` for each wrapped type,
17/// - a [`ForgeError`] impl whose methods delegate directly to the
18///   wrapped source's `ForgeError` methods (no type-erased
19///   downcast, no fallback values).
20///
21/// # Example
22///
23/// ```
24/// use error_forge::{group, AppError};
25///
26/// // `AppError` already implements `ForgeError`, so it can be
27/// // wrapped directly. Other types you wrap with `group!` must
28/// // also implement `ForgeError` (use `define_errors!` or
29/// // `#[derive(ModError)]` to produce them).
30/// group! {
31///     #[derive(Debug)]
32///     pub enum ServiceError {
33///         App(AppError),
34///     }
35/// }
36///
37/// // `From<AppError>` is generated, so `?` works against any
38/// // function that returns an `AppError`.
39/// let _err: ServiceError = AppError::config("missing").into();
40/// ```
41///
42/// # `ForgeError` requirement
43///
44/// Each wrapped source type must implement [`ForgeError`]. If you
45/// need to compose with a type that does not (e.g. `std::io::Error`),
46/// wrap it once in a `define_errors!` enum variant or
47/// `#[derive(ModError)]` enum and then group the result.
48///
49/// This is a **breaking change from `0.9.x`**, where `group!`
50/// accepted any wrapped type but the resulting `ForgeError` impl
51/// was silently incorrect — every method returned default
52/// fallback values regardless of the wrapped type. The `1.0`
53/// rewrite removes the type-erased downcast and trades it for a
54/// trait bound the compiler can verify.
55///
56/// [`ForgeError`]: crate::error::ForgeError
57#[macro_export]
58macro_rules! group {
59    (
60        $(#[$meta:meta])*
61        $vis:vis enum $name:ident {
62            $(
63                $(#[$vmeta:meta])*
64                $variant:ident($source_type:ty)
65            ),* $(,)?
66        }
67    ) => {
68        $(#[$meta])*
69        $vis enum $name {
70            $(
71                $(#[$vmeta])*
72                $variant($source_type),
73            )*
74        }
75
76        impl ::std::fmt::Display for $name {
77            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
78                match self {
79                    $(
80                        Self::$variant(source) => ::std::fmt::Display::fmt(source, f),
81                    )*
82                }
83            }
84        }
85
86        impl ::std::error::Error for $name {
87            fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> {
88                match self {
89                    $(
90                        Self::$variant(source) => {
91                            ::std::option::Option::Some(source as &(dyn ::std::error::Error + 'static))
92                        }
93                    )*
94                }
95            }
96        }
97
98        $(
99            impl ::std::convert::From<$source_type> for $name {
100                fn from(source: $source_type) -> Self {
101                    Self::$variant(source)
102                }
103            }
104        )*
105
106        impl $crate::error::ForgeError for $name {
107            fn kind(&self) -> &'static str {
108                match self {
109                    $(
110                        Self::$variant(source) => $crate::error::ForgeError::kind(source),
111                    )*
112                }
113            }
114
115            fn caption(&self) -> &'static str {
116                match self {
117                    $(
118                        Self::$variant(source) => $crate::error::ForgeError::caption(source),
119                    )*
120                }
121            }
122
123            fn is_retryable(&self) -> bool {
124                match self {
125                    $(
126                        Self::$variant(source) => $crate::error::ForgeError::is_retryable(source),
127                    )*
128                }
129            }
130
131            fn is_fatal(&self) -> bool {
132                match self {
133                    $(
134                        Self::$variant(source) => $crate::error::ForgeError::is_fatal(source),
135                    )*
136                }
137            }
138
139            fn status_code(&self) -> u16 {
140                match self {
141                    $(
142                        Self::$variant(source) => $crate::error::ForgeError::status_code(source),
143                    )*
144                }
145            }
146
147            fn exit_code(&self) -> i32 {
148                match self {
149                    $(
150                        Self::$variant(source) => $crate::error::ForgeError::exit_code(source),
151                    )*
152                }
153            }
154
155            fn user_message(&self) -> ::std::string::String {
156                match self {
157                    $(
158                        Self::$variant(source) => $crate::error::ForgeError::user_message(source),
159                    )*
160                }
161            }
162
163            fn dev_message(&self) -> ::std::string::String {
164                match self {
165                    $(
166                        Self::$variant(source) => $crate::error::ForgeError::dev_message(source),
167                    )*
168                }
169            }
170        }
171    };
172}