use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, ImplItem, ItemFn, ItemImpl, LitStr, ReturnType, Type};
#[proc_macro_attribute]
pub fn error_path(attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let path = if attr.is_empty() {
input.sig.ident.to_string()
} else {
parse_macro_input!(attr as LitStr).value()
};
expand_fn(input, path).into()
}
#[proc_macro_attribute]
pub fn error_path_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(item as ItemImpl);
let base_path = if attr.is_empty() {
infer_impl_name(&input)
} else {
parse_macro_input!(attr as LitStr).value()
};
for item in &mut input.items {
if let ImplItem::Fn(method) = item {
if take_skip_attr(&mut method.attrs) {
continue;
}
if !returns_result(&method.sig.output) {
continue;
}
let base_path_lit = LitStr::new(&base_path, method.sig.ident.span());
let method_path_lit =
LitStr::new(&method.sig.ident.to_string(), method.sig.ident.span());
let block = &method.block;
if method.sig.asyncness.is_some() {
method.block = syn::parse_quote!({
let __error_path_result = (async #block).await;
__error_path_result.map_err(|e| {
let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
})
});
} else {
method.block = syn::parse_quote!({
let __error_path_result = (|| #block)();
__error_path_result.map_err(|e| {
let e = ::error_path::WithErrorPath::with_error_path(e, #method_path_lit);
::error_path::WithErrorPath::with_error_path(e, #base_path_lit)
})
});
}
}
}
quote!(#input).into()
}
#[proc_macro_attribute]
pub fn error_path_skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
fn expand_fn(input: ItemFn, path: String) -> proc_macro2::TokenStream {
let attrs = input.attrs;
let vis = input.vis;
let sig = input.sig;
let block = input.block;
let path_lit = LitStr::new(&path, sig.ident.span());
if sig.asyncness.is_some() {
quote! {
#(#attrs)*
#vis #sig {
let __error_path_result = (async #block).await;
__error_path_result.map_err(|e| {
::error_path::WithErrorPath::with_error_path(e, #path_lit)
})
}
}
} else {
quote! {
#(#attrs)*
#vis #sig {
let __error_path_result = (|| #block)();
__error_path_result.map_err(|e| {
::error_path::WithErrorPath::with_error_path(e, #path_lit)
})
}
}
}
}
fn returns_result(output: &ReturnType) -> bool {
let ReturnType::Type(_, output_type) = output else {
return false;
};
let Type::Path(type_path) = output_type.as_ref() else {
return false;
};
type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident.to_string().ends_with("Result"))
}
fn infer_impl_name(input: &ItemImpl) -> String {
if let Some((_, trait_path, _)) = &input.trait_ {
return trait_path
.segments
.last()
.map(|seg| seg.ident.to_string())
.unwrap_or_else(|| "impl".to_string());
}
input.self_ty.to_token_stream().to_string().replace(' ', "")
}
fn take_skip_attr(attrs: &mut Vec<syn::Attribute>) -> bool {
let before = attrs.len();
attrs.retain(|attr| {
attr.path()
.segments
.last()
.map(|seg| seg.ident != "error_path_skip")
.unwrap_or(true)
});
attrs.len() != before
}