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}