error_stack_derive/lib.rs
1//! The heart of this crate is [`ErrorStack`],
2//! its a derive macro to make generating enums and structs compatible
3//! with [error_stack](https://docs.rs/error-stack/latest/error_stack/).
4//! Even though the sole purpose is provind a better DX with `error_stack`
5//! this derive macro can actually be used with any other error system
6//! since the crate itself doesn't depend on `error_stack` all it does
7//! is makes [`std::error::Error`] and [`std::fmt::Display`] implementations
8//! easy.
9//!
10//! ## Usage with and without `error_stack`
11//!
12//! ### With
13//!
14//! > Note:
15//! > As of right-now `no-std` is not supported
16//!
17//! With `error_stack` you get the `Report` and their fancy attachments,
18//! context, frames, etc. features, which to say the least are
19//! pretty cool and helpful for error handling & debugging.
20//!
21//! ```
22//! use error_stack::{IntoReport, Result, ResultExt};
23//! use error_stack_derive::ErrorStack;
24//!
25//! #[derive(ErrorStack, Debug)]
26//! #[error_message("An exception occured in foo")]
27//! struct FooError;
28//!
29//! fn main() -> Result<(), FooError> {
30//! let contents = std::fs::read_to_string("foo.txt")
31//! .report()
32//! .change_context(FooError)
33//! .attach_printable("Unable to read foo.txt file")?;
34//!
35//! println!("{contents}");
36//!
37//! Ok(())
38//! }
39//! ```
40//!
41//! ### Without
42//!
43//! Ofcourse this crate doesn't enforce the usage of `error_stack`
44//! infact you can use it with any other error handling crate,
45//! just like this
46//!
47//! ```
48//! use error_stack_derive::ErrorStack;
49//!
50//! #[derive(ErrorStack, Debug)]
51//! #[error_message(&format!("An exception occured with foo: {}", self.0))]
52//!
53//! struct FooError(String);
54//! fn main() -> Result<(), FooError> {
55//! let contents = std::fs::read_to_string("foo.txt").map_err(|e| FooError(e.to_string()))?;
56//!
57//! println!("{contents}");
58//!
59//! Ok(())
60//! }
61//! ```
62//!
63//! ## Looking into the expansion
64//!
65//! This crate, specifically the derive macro, does 2 things, <br />
66//! one, implements [`std::error::Error`] <br />
67//! two, implements [`std::fmt::Display`] <br />
68//! you can derive a struct or an enum, the trait impl are pretty
69//! simple
70//!
71//! For a struct:
72//!
73//! ```
74//! // #[derive(error_stack_derive::ErrorStack, Debug)]
75//! // #[error_message("An exception occured in foo")]
76//! // struct FooError;
77//!
78//! #[derive(Debug)]
79//! struct FooError;
80//!
81//! impl std::fmt::Display for FooError {
82//! fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83//! fmt.write_str("An exception occured in foo")
84//! }
85//! }
86//!
87//! impl std::error::Error for FooError {}
88//! ```
89//!
90//! For an enum:
91//!
92//! ```
93//! // #[derive(error_stack_derive::ErrorStack, Debug)]
94//! // enum FooErrors {
95//! // #[error_message("An exception in bar")]
96//! // BarError,
97//! // #[error_message(&format!("Error in baz ({unnamed0})"))]
98//! // BazError(String),
99//! // #[error_message(&format!("Error in qux ({start}, {end})"))]
100//! // QuxError {
101//! // start: u64,
102//! // end: u64,
103//! // }
104//! // };
105//!
106//! #[derive(Debug)]
107//! enum FooErrors {
108//! BarError,
109//! BazError(String),
110//! QuxError {
111//! start: u64,
112//! end: u64,
113//! }
114//! };
115//!
116//! impl std::fmt::Display for FooErrors {
117//! fn fmt(&self, _____fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118//! match self {
119//! Self::BarError => _____fmt.write_str(&format!("[{name}] An error occured; {:?}", name = "FooErrors", self)),
120//! Self::BazError(unnamed0) => _____fmt.write_str(&format!("Error in baz ({unnamed0})")),
121//! Self::QuxError { start, end } => _____fmt.write_str(&format!("Error in qux ({start}, {end})")),
122//! }
123//! }
124//! }
125//!
126//! impl std::error::Error for FooErrors {}
127//! ```
128//!
129//! As you can see its a pretty simple macro but definitely helps when
130//! you have a large code base and error handling definitely becomes dreadful.
131//! Read up the doc comments of [`ErrorStack`] for more information.
132//!
133use proc_macro::TokenStream;
134use quote::{quote, TokenStreamExt};
135use syn::{parse, parse_str, Attribute, Data, DataEnum, DeriveInput, Fields, Generics, Ident};
136
137/// A derive-macro to easily create enums and structs compatible with
138/// error_stack. You can use a struct or an enum with it
139///
140///
141/// ## Panic
142///
143/// - When input cannot be passed as [`syn::DeriveInput`]
144/// - When the derive data is not one of [`syn::Data::Enum`] or
145/// [`syn::Data::Struct`]
146///
147///
148/// ## Usage
149///
150/// ```
151/// use error_stack_derive::ErrorStack;
152///
153/// #[derive(ErrorStack, Debug)]
154/// // error_message tokens can be any token stream as long as it evaluates
155/// // to a &str
156/// #[error_message("An error occured in Foo")]
157/// struct FooError;
158///
159/// #[derive(ErrorStack, Debug)]
160/// // The tokens are passed to the [`std::fmt::Formatter::write_str`]
161/// // method of the [`std::fmt::Formatter`] in the automatically
162/// // implemented Display impl. Passing an error message is mandatory
163/// // for structs while its not for enums
164/// // So you can do this too!
165/// #[error_message(&format!("An internal error occured: {}", self.0))]
166/// struct InternalError<A>(pub A)
167/// where
168/// A: std::fmt::Display + std::fmt::Debug + Send + Sync + 'static;
169///
170///
171/// // And ofcourse enums are supported too
172/// #[derive(ErrorStack, Debug)]
173/// // This is the default error message, this is used when a variant
174/// // doesn't have a dedicated error message
175/// // When a default error message is not specified and an enum doesn't
176/// // have a dedicated message,
177/// // `&format!("[{name}] An error occured; {:?}", name = #struct_name, self)` is passed to
178/// // [`std::fmt::Formatter::write_str`]
179/// #[error_message("Default error message")]
180/// enum EncoderError {
181/// // For struct variants the name of the fields are left unchanged
182/// // but for tuple variants they are named `unnamed{pos}`
183/// #[error_message(&format!("Couldn't serialize data: {:?}", unnamed0))]
184/// SerializeError(String),
185/// DeserializeError,
186/// }
187/// ```
188#[proc_macro_derive(ErrorStack, attributes(error_message))]
189pub fn error(tokens: TokenStream) -> TokenStream {
190 let DeriveInput {
191 attrs,
192 vis: _,
193 ident,
194 generics,
195 data,
196 } = parse(tokens).expect("derive input");
197
198 let ast = match data {
199 Data::Enum(data) => create_enum(attrs, ident, generics, data),
200 Data::Struct(_) => create_struct(attrs, ident, generics),
201 _ => panic!("#[derive(ErrorStack)] only supports structs and enums"),
202 };
203
204 ast.into()
205}
206
207fn create_enum(
208 attrs: Vec<Attribute>,
209 ident: Ident,
210 Generics {
211 lt_token,
212 params,
213 gt_token,
214 where_clause,
215 }: Generics,
216 DataEnum {
217 enum_token: _,
218 brace_token: _,
219 variants,
220 }: DataEnum,
221) -> TokenStream {
222 let message = match attrs
223 .iter()
224 .find(|attr| attr.path.is_ident("error_message"))
225 {
226 Some(attr) => attr.tokens.to_owned(),
227 None => {
228 let name = syn::LitStr::new(&ident.to_string(), ident.span());
229 quote!(&format!(
230 "[{name}] An error occured; {:?}",
231 self,
232 name = #name,
233 ))
234 }
235 };
236
237 let match_arms = {
238 let mut tmp = quote!();
239 tmp.append_all(variants.iter().filter_map(|variant| {
240 let ident = variant.ident.to_owned();
241 let message = variant.attrs.iter().find_map(|attr| {
242 if attr.path.is_ident("error_message") {
243 return Some(attr.tokens.to_owned());
244 }
245 None
246 });
247
248 let additional = match variant.fields {
249 Fields::Named(ref named) => {
250 let mut tmp = quote!();
251 tmp.append_all(named.named.iter().map(|field| {
252 let ident = field.ident.to_owned();
253 quote! {
254 #ident ,
255 }
256 }));
257 quote! {{
258 #tmp
259 }}
260 }
261 Fields::Unnamed(ref unnamed) => {
262 let mut tmp = quote!();
263 tmp.append_all(unnamed.unnamed.iter().enumerate().map(|(pos, _)| {
264 let ident: Ident = parse_str(&format!("unnamed{pos}")).unwrap();
265 quote! {
266 #ident ,
267 }
268 }));
269 quote! {(#tmp)}
270 }
271 Fields::Unit => quote!(),
272 };
273
274 match message {
275 Some(tokens) => Some(quote! {
276 Self::#ident #additional => _____fmt.write_str(#tokens),
277 }),
278 None => None,
279 }
280 }));
281 tmp
282 };
283
284 quote! {
285 impl #lt_token #params #gt_token std::fmt::Display for #ident #lt_token #params #gt_token #where_clause {
286 fn fmt(&self, _____fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287 #[allow(unused_parens)]
288 match self {
289 #match_arms
290 _ => _____fmt.write_str(#message)
291 }
292 }
293 }
294
295 impl #lt_token #params #gt_token std::error::Error for #ident #lt_token #params #gt_token #where_clause {}
296 }
297 .into()
298}
299
300fn create_struct(
301 attrs: Vec<Attribute>,
302 ident: Ident,
303 Generics {
304 lt_token,
305 params,
306 gt_token,
307 where_clause,
308 }: Generics,
309) -> TokenStream {
310 let message = attrs
311 .iter()
312 .find(|attr| attr.path.is_ident("error_message"))
313 .expect("expected error message")
314 .tokens
315 .to_owned();
316
317 quote! {
318 impl #lt_token #params #gt_token std::fmt::Display for #ident #lt_token #params #gt_token #where_clause {
319 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 #[allow(unused_parens)]
321 fmt.write_str(#message)
322 }
323 }
324
325 impl #lt_token #params #gt_token std::error::Error for #ident #lt_token #params #gt_token #where_clause {}
326 }
327 .into()
328}