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}