pipex_macros/
lib.rs

1//! # Pipex Macros
2//! 
3//! Procedural macros for the pipex crate, providing error handling strategies
4//! and pipeline decorators for async and sync functions.
5
6extern crate proc_macro;
7
8use proc_macro::TokenStream;
9use quote::quote;
10use syn::{
11    parse_macro_input, ItemFn, Type, ReturnType, GenericArgument, PathArguments,
12    parse::Parse, parse::ParseStream, Error, Result as SynResult
13};
14
15/// Parser for attribute arguments
16struct AttributeArgs {
17    strategy_type: Type,
18}
19
20impl Parse for AttributeArgs {
21    fn parse(input: ParseStream) -> SynResult<Self> {
22        let strategy_type: Type = input.parse()?;
23        Ok(AttributeArgs { strategy_type })
24    }
25}
26
27/// Extract the inner types from Result<T, E>
28fn extract_result_types(return_type: &Type) -> SynResult<(Type, Type)> {
29    if let Type::Path(type_path) = return_type {
30        if let Some(segment) = type_path.path.segments.last() {
31            if segment.ident == "Result" {
32                if let PathArguments::AngleBracketed(args) = &segment.arguments {
33                    if args.args.len() == 2 {
34                        if let (
35                            GenericArgument::Type(ok_type),
36                            GenericArgument::Type(err_type)
37                        ) = (&args.args[0], &args.args[1]) {
38                            return Ok((ok_type.clone(), err_type.clone()));
39                        }
40                    }
41                }
42            }
43        }
44    }
45    
46    Err(Error::new_spanned(
47        return_type,
48        "Expected function to return Result<T, E>"
49    ))
50}
51
52/// The `error_strategy` attribute macro
53/// 
54/// This macro transforms a function that returns `Result<T, E>` into one that 
55/// returns `PipexResult<T, E>`, allowing the pipex library to apply the specified
56/// error handling strategy. Works with both sync and async functions.
57/// 
58/// # Arguments
59/// 
60/// * `strategy` - The error handling strategy type (e.g., `IgnoreHandler`, `CollectHandler`)
61/// 
62/// # Examples
63/// 
64/// ```rust,ignore
65/// use pipex_macros::error_strategy;
66/// 
67/// // Async function
68/// #[error_strategy(IgnoreHandler)]
69/// async fn process_item_async(x: i32) -> Result<i32, String> {
70///     if x % 2 == 0 {
71///         Ok(x * 2)
72///     } else {
73///         Err("Odd number".to_string())
74///     }
75/// }
76/// 
77/// // Sync function  
78/// #[error_strategy(CollectHandler)]
79/// fn process_item_sync(x: i32) -> Result<i32, String> {
80///     if x % 2 == 0 {
81///         Ok(x * 2)
82///     } else {
83///         Err("Odd number".to_string())
84///     }
85/// }
86/// ```
87/// 
88/// The generated function will automatically wrap the result in a `PipexResult`
89/// with the specified strategy name, allowing the pipeline to handle errors
90/// according to the strategy.
91#[proc_macro_attribute]
92pub fn error_strategy(args: TokenStream, item: TokenStream) -> TokenStream {
93    let input_fn = parse_macro_input!(item as ItemFn);
94    let args = parse_macro_input!(args as AttributeArgs);
95    
96    let strategy_type = args.strategy_type;
97    let fn_name = &input_fn.sig.ident;
98    let fn_vis = &input_fn.vis;
99    let fn_inputs = &input_fn.sig.inputs;
100    let fn_body = &input_fn.block;
101    let fn_asyncness = &input_fn.sig.asyncness;
102    let fn_generics = &input_fn.sig.generics;
103    let where_clause = &input_fn.sig.generics.where_clause;
104    
105    // Extract return type and validate it's Result<T, E>
106    let (ok_type, err_type) = match &input_fn.sig.output {
107        ReturnType::Type(_, ty) => {
108            match extract_result_types(ty) {
109                Ok(types) => types,
110                Err(e) => return e.to_compile_error().into(),
111            }
112        }
113        ReturnType::Default => {
114            return Error::new_spanned(
115                &input_fn.sig,
116                "Function must return Result<T, E>"
117            ).to_compile_error().into();
118        }
119    };
120    
121    // Create hidden function name for original implementation
122    let original_impl_name = syn::Ident::new(
123        &format!("{}_original_impl", fn_name),
124        fn_name.span()
125    );
126    
127    // Use the strategy type name as the strategy identifier
128    let strategy_name = quote!(#strategy_type).to_string();
129    
130    // Extract parameter names for the function call
131    let param_names: Vec<_> = input_fn.sig.inputs.iter().filter_map(|arg| {
132        if let syn::FnArg::Typed(pat_type) = arg {
133            if let syn::Pat::Ident(pat_ident) = &*pat_type.pat {
134                Some(&pat_ident.ident)
135            } else {
136                None
137            }
138        } else {
139            None
140        }
141    }).collect();
142    
143    // Generate different code based on whether function is async or sync
144    let function_call = if fn_asyncness.is_some() {
145        // Async function - use .await
146        quote! { #original_impl_name(#(#param_names),*).await }
147    } else {
148        // Sync function - no .await
149        quote! { #original_impl_name(#(#param_names),*) }
150    };
151    
152    let expanded = quote! {
153        #[doc(hidden)]
154        #fn_asyncness fn #original_impl_name #fn_generics (#fn_inputs) -> Result<#ok_type, #err_type> #where_clause
155        #fn_body
156        
157        #fn_vis #fn_asyncness fn #fn_name #fn_generics (#fn_inputs) -> crate::PipexResult<#ok_type, #err_type> #where_clause {
158            let result = #function_call;
159            crate::PipexResult::new(result, #strategy_name)
160        }
161    };
162    
163    TokenStream::from(expanded)
164}
165
166// No tests in proc macro crate - they can't use the macros defined here
167// Tests will be in the main pipex crate or integration tests