Skip to main content

error_path_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{parse_macro_input, ImplItem, ItemFn, ItemImpl, LitStr, ReturnType, Type};
4
5/// Attach an error path to a single Result-returning function.
6///
7/// - `#[error_path]` uses the function name.
8/// - `#[error_path("custom.path")]` uses the provided path.
9#[proc_macro_attribute]
10pub fn error_path(attr: TokenStream, item: TokenStream) -> TokenStream {
11    let input = parse_macro_input!(item as ItemFn);
12
13    let path = if attr.is_empty() {
14        input.sig.ident.to_string()
15    } else {
16        parse_macro_input!(attr as LitStr).value()
17    };
18
19    expand_fn(input, path).into()
20}
21
22/// Attach error paths to every Result-returning method in an impl block.
23///
24/// - `#[error_path_impl]` uses the trait name for trait impls, otherwise the type name.
25/// - `#[error_path_impl("custom.prefix")]` uses the provided prefix.
26/// - `#[error_path_skip]` can be placed on methods that should not be wrapped.
27#[proc_macro_attribute]
28pub fn error_path_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
29    let mut input = parse_macro_input!(item as ItemImpl);
30
31    let base_path = if attr.is_empty() {
32        infer_impl_name(&input)
33    } else {
34        parse_macro_input!(attr as LitStr).value()
35    };
36
37    for item in &mut input.items {
38        if let ImplItem::Fn(method) = item {
39            if take_skip_attr(&mut method.attrs) {
40                continue;
41            }
42
43            if !returns_result(&method.sig.output) {
44                continue;
45            }
46
47            let base_path_lit = LitStr::new(&base_path, method.sig.ident.span());
48            let method_path_lit =
49                LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span());
50            let block = &method.block;
51
52            if method.sig.asyncness.is_some() {
53                method.block = syn::parse_quote!({
54                    let __error_path_result = (async #block).await;
55                    __error_path_result.map_err(|e| {
56                        let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
57                        ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
58                    })
59                });
60            } else {
61                method.block = syn::parse_quote!({
62                    let __error_path_result = (|| #block)();
63                    __error_path_result.map_err(|e| {
64                        let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
65                        ::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
66                    })
67                });
68            }
69        }
70    }
71
72    quote!(#input).into()
73}
74
75/// Marker attribute for methods inside `#[error_path_impl]` blocks.
76///
77/// This attribute is consumed by `#[error_path_impl]`.
78#[proc_macro_attribute]
79pub fn error_path_skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
80    item
81}
82
83fn expand_fn(input: ItemFn, path: String) -> proc_macro2::TokenStream {
84    let attrs = input.attrs;
85    let vis = input.vis;
86    let sig = input.sig;
87    let block = input.block;
88    let path_lit = LitStr::new(&path, sig.ident.span());
89
90    if sig.asyncness.is_some() {
91        quote! {
92            #(#attrs)*
93            #vis #sig {
94                let __error_path_result = (async #block).await;
95                __error_path_result.map_err(|e| {
96                    ::error_path::WithErrorPath::with_error_path(e, #path_lit)
97                })
98            }
99        }
100    } else {
101        quote! {
102            #(#attrs)*
103            #vis #sig {
104                let __error_path_result = (|| #block)();
105                __error_path_result.map_err(|e| {
106                    ::error_path::WithErrorPath::with_error_path(e, #path_lit)
107                })
108            }
109        }
110    }
111}
112
113fn returns_result(output: &ReturnType) -> bool {
114    let ReturnType::Type(_, output_type) = output else {
115        return false;
116    };
117    let Type::Path(type_path) = output_type.as_ref() else {
118        return false;
119    };
120
121    type_path
122        .path
123        .segments
124        .last()
125        .is_some_and(|segment| segment.ident.to_string().ends_with("Result"))
126}
127
128fn infer_impl_name(input: &ItemImpl) -> String {
129    if let Some((_, trait_path, _)) = &input.trait_ {
130        return trait_path
131            .segments
132            .last()
133            .map(|seg| seg.ident.to_string())
134            .unwrap_or_else(|| "impl".to_string());
135    }
136
137    input.self_ty.to_token_stream().to_string().replace(' ', "")
138}
139
140fn take_skip_attr(attrs: &mut Vec<syn::Attribute>) -> bool {
141    let before = attrs.len();
142    attrs.retain(|attr| {
143        attr.path()
144            .segments
145            .last()
146            .map(|seg| seg.ident != "error_path_skip")
147            .unwrap_or(true)
148    });
149    attrs.len() != before
150}