errgo/
lib.rs

1//! Generate `enum` error variants inline.
2//!
3//! A slightly type-safer take on [anyhow], where each ad-hoc error is handleable by the caller.
4//! Designed to play nice with other crates like [strum] or [thiserror].
5//!
6//! This crate was written to aid wrapping C APIs - transforming e.g error codes to handleable messages.
7//! It shouldn't really be used for library api entry points - a well-considered top-level error type is likely to be both more readable and forward compatible.
8//! Consider reading [Study of `std::io::Error`](https://matklad.github.io/2020/10/15/study-of-std-io-error.html) or simply making all generated structs `pub(crate)`.
9//!
10//! ```
11//! use errgo::errgo;
12//!
13//! #[errgo]
14//! fn shave_yaks(
15//!     num_yaks: usize,
16//!     empty_buckets: usize,
17//!     num_razors: usize,
18//! ) -> Result<(), ShaveYaksError> {
19//!     if num_razors == 0 {
20//!         return Err(err!(NotEnoughRazors));
21//!     }
22//!     if num_yaks > empty_buckets {
23//!         return Err(err!(NotEnoughBuckets {
24//!             got: usize = empty_buckets,
25//!             required: usize = num_yaks,
26//!         }));
27//!     }
28//!     Ok(())
29//! }
30//! ```
31//! Under the hood, a struct like this is generated:
32//! ```
33//! enum ShaveYaksError { // name and visibility are taken from function return type and visibility
34//!     NotEnoughRazors,
35//!     NotEnoughBuckets {
36//!         got: usize,
37//!         required: usize,
38//!     }
39//! }
40//! ```
41//! Note that the struct definition is placed just above the function body, meaning that you can't use [`errgo`] on functions in `impl` blocks - you'll have to move the function body to an outer scope, and call it in the impl block.
42//!
43//!
44//! Importantly, you can derive on the generated struct, _and_ passthrough attributes, allowing you to use crates like [thiserror] or [strum].
45//! See the [`errgo`] documentation for other arguments accepted by the macro.
46//! ```
47//! # use errgo::errgo;
48//!
49//! #[errgo(derive(Debug, thiserror::Error))]
50//! fn shave_yaks(
51//!     num_yaks: usize,
52//!     empty_buckets: usize,
53//!     num_razors: usize,
54//! ) -> Result<(), ShaveYaksError> {
55//!     if num_razors == 0 {
56//!         return Err(err!(
57//!             #[error("not enough razors!")]
58//!             NotEnoughRazors
59//!         ));
60//!     }
61//!     if num_yaks > empty_buckets {
62//!         return Err(err!(
63//!             #[error("not enough buckets - needed {required}")]
64//!             NotEnoughBuckets {
65//!                 got: usize = empty_buckets,
66//!                 required: usize = num_yaks,
67//!             }
68//!         ));
69//!     }
70//!     Ok(())
71//! }
72//! ```
73//!
74//! Which generates the following:
75//! ```
76//! #[derive(Debug, thiserror::Error)]
77//! enum ShaveYaksError {
78//!     #[error("not enough razors!")]
79//!     NotEnoughRazors,
80//!     #[error("not enough buckets - needed {required}")]
81//!     NotEnoughBuckets {
82//!         got: usize,
83//!         required: usize,
84//!     }
85//! }
86//! ```
87//! And `err!` macro invocations are replaced with struct instantiations - no matter where they are in the function body!
88//!
89//! If you need to reuse the same variant within a function, just use the normal construction syntax:
90//! ```
91//! # use errgo::errgo;
92//! # use std::io;
93//! # fn fallible_op() -> Result<(), io::Error> { todo!() }
94//! #[errgo]
95//! fn foo() -> Result<(), FooError> {
96//!     fallible_op().map_err(|e| err!(IoError(io::Error = e)));
97//!     Err(FooError::IoError(todo!()))
98//! }
99//! ```
100//!
101//! [anyhow]: https://docs.rs/anyhow
102//! [thiserror]: https://docs.rs/thiserror
103//! [strum]: https://docs.rs/strum
104
105use config::Config;
106use data::VariantWithValue;
107use proc_macro2::{Ident, Span, TokenStream};
108use proc_macro_error::{emit_error, proc_macro_error};
109use quote::{quote, ToTokens};
110use syn::{
111    parse2, parse_macro_input, visit_mut::VisitMut, AngleBracketedGenericArguments,
112    GenericArgument, ItemFn, Path, PathArguments, PathSegment, ReturnType, TypePath,
113};
114
115mod config;
116mod data;
117
118/// See [module documentation](index.html) for general usage.
119///
120/// # `err!` construction
121/// Instances of `err!` will be parsed like so:
122/// ```
123/// # #[errgo::errgo]
124/// # fn foo() -> Result<(), FooError> {
125/// err!(Unity);                        // A unit enum variant
126/// err!(Tuply(usize = 1, char = 'a')); // A tuple enum variant
127/// err!(Structy {                      // A struct enum variant
128///         u: usize = 1,
129///         c: char = 'a',
130/// });
131/// # Ok(())
132/// # }
133/// ```
134/// # Arguments
135/// `derive` arguments are passed through to the generated struct.
136/// ```
137/// # use errgo::errgo;
138/// #[errgo(derive(Debug, Clone, Copy))]
139/// # fn foo() -> Result<(), FooError> { Ok(()) }
140/// ```
141///
142/// `attributes` arguments are passed through to the top of the generated struct
143/// ```
144/// # use errgo::errgo;
145/// #[errgo(attributes(
146///     #[must_use = "maybe you missed something!"]
147///     #[repr(u8)]
148/// ))]
149/// # fn foo() -> Result<(), FooError> { Ok(()) }
150/// ```
151/// `visibility` can be used to override the generated struct's visibility.
152/// ```
153/// # use errgo::errgo;
154/// #[errgo(visibility(pub))]
155/// # fn foo() -> Result<(), FooError> { Ok(()) }
156/// ```
157#[proc_macro_attribute]
158#[proc_macro_error]
159pub fn errgo(
160    attr: proc_macro::TokenStream,
161    item: proc_macro::TokenStream,
162) -> proc_macro::TokenStream {
163    // Parse our inputs
164    let config = parse_macro_input!(attr as Config);
165    let mut item = parse_macro_input!(item as ItemFn);
166
167    let Some(error_name) = get_struct_name_from_return_type(&item.sig.output) else {
168        emit_error!(
169            item.sig,
170            "unsupported return type - function must return a `Result<_, SomeConcreteErr>`"
171        );
172        return quote!(#item).into();
173    };
174    let error_vis = config.visibility.unwrap_or_else(|| item.vis.clone());
175
176    // Make the changes to the syntax tree, and collect the error variants
177    let mut visitor = ErrAsYouGoVisitor::new(error_name.clone());
178    visitor.visit_item_fn_mut(&mut item);
179
180    for (src, e) in visitor.collection_errors {
181        emit_error!(src, "{}", e)
182    }
183
184    // Assemble our output
185    let variants = visitor.variants;
186    let derives = match config.derives {
187        Some(derives) => quote!(#[derive(
188            #(#derives),*
189        )]),
190        None => quote!(),
191    };
192
193    quote! {
194        #derives
195        #error_vis enum #error_name {
196            #(#variants),*
197        }
198
199        #item
200    }
201    .into()
202}
203
204fn get_struct_name_from_return_type(return_type: &ReturnType) -> Option<Ident> {
205    if let ReturnType::Type(_, ty) = return_type {
206        if let syn::Type::Path(TypePath {
207            qself: None,
208            path: Path { ref segments, .. },
209        }) = **ty
210        {
211            if let Some(PathSegment {
212                ident,
213                arguments:
214                    PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
215            }) = segments.last()
216            {
217                if ident == "Result" && args.len() == 2 {
218                    if let Some(GenericArgument::Type(syn::Type::Path(TypePath {
219                        qself: None,
220                        path:
221                            Path {
222                                segments,
223                                leading_colon: None,
224                            },
225                    }))) = args.into_iter().nth(1)
226                    {
227                        if segments.len() == 1 {
228                            let PathSegment { ident, arguments } = &segments[0];
229                            if arguments.is_empty() {
230                                return Some(ident.clone());
231                            }
232                        }
233                    }
234                }
235            }
236        }
237    }
238    None
239}
240
241/// Implementation detail
242// Allows use to swap the macro in-place in our visitor.
243#[doc(hidden)]
244#[proc_macro]
245#[proc_macro_error]
246pub fn __nothing(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
247    input
248}
249
250struct ErrAsYouGoVisitor {
251    error_name: Ident,
252    variants: Vec<syn::Variant>,
253    collection_errors: Vec<(TokenStream, syn::Error)>,
254}
255
256impl ErrAsYouGoVisitor {
257    fn new(error_name: Ident) -> Self {
258        Self {
259            error_name,
260            variants: Vec::new(),
261            collection_errors: Vec::new(),
262        }
263    }
264}
265
266impl syn::visit_mut::VisitMut for ErrAsYouGoVisitor {
267    fn visit_macro_mut(&mut self, i: &mut syn::Macro) {
268        if i.path.is_ident("err") || i.path.is_ident("errgo") {
269            match parse2::<VariantWithValue>(i.tokens.clone()) {
270                Ok(variant_with_value) => {
271                    self.variants
272                        .push(variant_with_value.clone().into_syn_variant());
273                    i.path = path(["errgo", "__nothing"]);
274                    i.tokens = variant_with_value
275                        .into_syn_expr_with_prefix(Path::from(self.error_name.clone()))
276                        .into_token_stream();
277                }
278                Err(e) => self.collection_errors.push((i.tokens.clone(), e)),
279            }
280        }
281    }
282}
283
284fn path<'a>(segments: impl IntoIterator<Item = &'a str>) -> Path {
285    syn::Path {
286        leading_colon: None,
287        segments: segments
288            .into_iter()
289            .map(|segment| PathSegment::from(ident(segment)))
290            .collect(),
291    }
292}
293
294fn ident(s: &str) -> Ident {
295    Ident::new(s, Span::call_site())
296}
297
298#[cfg(test)]
299mod test_utils {
300
301    pub fn test_parse<T>(tokens: proc_macro2::TokenStream, expected: T)
302    where
303        T: syn::parse::Parse + PartialEq + std::fmt::Debug,
304    {
305        let actual = syn::parse2::<T>(tokens).expect("couldn't parse tokens");
306        pretty_assertions::assert_eq!(expected, actual);
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313    use pretty_assertions::assert_eq;
314
315    #[test]
316    fn trybuild() {
317        let t = trybuild::TestCases::new();
318        t.pass("trybuild/pass/**/*.rs");
319        t.compile_fail("trybuild/fail/**/*.rs")
320    }
321
322    #[test]
323    fn readme() {
324        let expected = std::process::Command::new("cargo")
325            .arg("readme")
326            .output()
327            .expect("couldn't run `cargo readme`")
328            .stdout;
329        let expected = String::from_utf8(expected).expect("`cargo readme` output wasn't UTF-8");
330        let actual = include_str!("../README.md");
331        assert_eq!(expected, actual);
332    }
333
334    #[test]
335    fn get_result_name() {
336        let ident = get_struct_name_from_return_type(
337            &syn::parse2(quote!(-> Result<T, SomeConcreteErr>)).unwrap(),
338        )
339        .unwrap();
340        assert_eq!(ident, "SomeConcreteErr");
341
342        let ident = get_struct_name_from_return_type(
343            &syn::parse2(quote!(-> ::std::result::Result<T, SomeConcreteErr>)).unwrap(),
344        )
345        .unwrap();
346        assert_eq!(ident, "SomeConcreteErr");
347    }
348}