Skip to main content

azure_keyvault_tui_macros/
lib.rs

1use darling::{FromMeta, ast::NestedMeta};
2use proc_macro::TokenStream;
3use proc_macro_error::{abort, proc_macro_error};
4use proc_macro2::TokenStream as TokenStream2;
5use quote::quote;
6use syn::{
7    Attribute, Data, DeriveInput, Fields, FnArg, ItemFn, Meta, Variant, parse_macro_input,
8    parse_quote, parse2, spanned::Spanned,
9};
10
11#[derive(FromMeta)]
12struct CallbackArgs {
13    event_enum: syn::Path,
14    error_variant: syn::Expr,
15}
16
17#[proc_macro_error]
18#[proc_macro_attribute]
19pub fn background_task(args: TokenStream, item: TokenStream) -> TokenStream {
20    let input = parse_macro_input!(item as ItemFn);
21    let attr_args = match NestedMeta::parse_meta_list(args.into()) {
22        Ok(args) => args,
23        Err(e) => abort!(e.span(), "Invalid attribute arguments: {}", e),
24    };
25    let args = match CallbackArgs::from_list(&attr_args) {
26        Ok(args) => args,
27        Err(e) => return TokenStream::from(e.write_errors()),
28    };
29    background_task_impl(input, args).into()
30}
31
32fn background_task_impl(mut function: ItemFn, args: CallbackArgs) -> TokenStream2 {
33    function
34        .sig
35        .inputs
36        .insert(0, event_sender_arg(&args.event_enum));
37    let update_progress_macro = update_progress_macro();
38    let abort_macro = abort_macro(&args.error_variant);
39    let block = function.block.clone();
40    function.block = parse2(quote! {
41        {
42            #update_progress_macro
43            #abort_macro
44            #block
45        }
46    })
47    .unwrap_or_else(|e| {
48        abort!(
49            proc_macro2::Span::mixed_site(),
50            "Failed to parse function block: {}",
51            e
52        );
53    });
54    quote! { #function }
55}
56
57fn event_sender_arg(event_type: &syn::Path) -> FnArg {
58    let event_type_ident: syn::Type = syn::parse_quote!(#event_type);
59    parse_quote!(tx: tokio::sync::mpsc::Sender<#event_type_ident>)
60}
61
62fn update_progress_macro() -> TokenStream2 {
63    quote! {
64        macro_rules! update_progress {
65            ($variant: expr) => {
66                if let Err(_) = tx.send($variant).await {
67                    return;
68                }
69            }
70        }
71    }
72}
73
74fn abort_macro(error_variant: &syn::Expr) -> TokenStream2 {
75    quote! {
76        macro_rules! abort {
77            ($($arg:tt)*) => {
78                let message = format!($($arg)*);
79                let _ = tx.send(#error_variant(message.clone())).await.inspect_err(|_| eprintln!("{}", message));
80                return;
81            }
82        }
83    }
84}
85
86#[derive(FromMeta)]
87struct EnumArgs {
88    event_enum: syn::Path,
89}
90
91#[derive(FromMeta)]
92struct VariantArgs {
93    callback: syn::Ident,
94}
95
96#[proc_macro_error]
97#[proc_macro_derive(BackgroundTaskSpec, attributes(taskspec))]
98pub fn background_task_spec_derive(input: TokenStream) -> TokenStream {
99    let input = parse_macro_input!(input as DeriveInput);
100    background_task_spec_impl(input).into()
101}
102
103fn background_task_spec_impl(input: DeriveInput) -> TokenStream2 {
104    let enum_name = &input.ident;
105
106    // Parse the event_type from the derive macro arguments
107    let event_type = extract_event_enum(&input.attrs);
108    let event_type_ident: syn::Type = syn::parse_quote!(#event_type);
109
110    let variants = match input.data {
111        Data::Enum(data_enum) => data_enum.variants,
112        _ => abort!(
113            input.ident.span(),
114            "BackgroundTaskSpec can only be derived for enums, found {}",
115            match input.data {
116                Data::Struct(_) => "struct",
117                Data::Union(_) => "union",
118                _ => "unknown type",
119            }
120        ),
121    };
122
123    let spawn_arms = variants.iter().map(generate_spawn_arm).collect::<Vec<_>>();
124
125    quote! {
126        impl #enum_name {
127            /// Spawns a background task for this task specification.
128            ///
129            /// This method consumes `self` to move the contained data into the spawned task.
130            /// Returns a `JoinHandle` that can be used to await completion of the background task.
131            pub fn spawn_task(self, tx: &tokio::sync::mpsc::Sender<#event_type_ident>) -> tokio::task::JoinHandle<()> {
132                match self {
133                    #(#spawn_arms)*
134                }
135            }
136        }
137    }
138}
139
140fn generate_spawn_arm(variant: &Variant) -> TokenStream2 {
141    let variant_name = &variant.ident;
142    let callback_name = extract_callback_name(&variant.attrs);
143
144    match &variant.fields {
145        Fields::Unit => {
146            if let Some(callback) = callback_name {
147                quote! {
148                    Self::#variant_name => {
149                        let tx_clone = tx.clone();
150                        tokio::task::spawn(async move {
151                            #callback(tx_clone).await;
152                        })
153                    }
154                }
155            } else {
156                quote! {
157                    Self::#variant_name => {}
158                }
159            }
160        }
161        Fields::Unnamed(fields) => {
162            let field_names: Vec<syn::Ident> = (0..fields.unnamed.len())
163                .map(|i| syn::Ident::new(&format!("field_{}", i), proc_macro2::Span::mixed_site()))
164                .collect();
165
166            let pattern = quote! { Self::#variant_name(#(#field_names),*) };
167
168            if let Some(callback) = callback_name {
169                let args = quote! { tx_clone, #(#field_names),* };
170                quote! {
171                    #pattern => {
172                        let tx_clone = tx.clone();
173                        tokio::task::spawn(async move {
174                            #callback(#args).await;
175                        })
176                    }
177                }
178            } else {
179                quote! {
180                    #pattern => {}
181                }
182            }
183        }
184        Fields::Named(fields) => {
185            let field_names: Vec<&syn::Ident> = fields
186                .named
187                .iter()
188                .filter_map(|f| f.ident.as_ref())
189                .collect();
190
191            if field_names.len() != fields.named.len() {
192                abort!(
193                    variant.ident.span(),
194                    "All named fields must have identifiers"
195                );
196            }
197
198            let pattern = quote! { Self::#variant_name { #(#field_names),* } };
199
200            if let Some(callback) = callback_name {
201                let args = quote! { tx_clone, #(#field_names),* };
202                quote! {
203                    #pattern => {
204                        let tx_clone = tx.clone();
205                        tokio::task::spawn(async move {
206                            #callback(#args).await;
207                        })
208                    }
209                }
210            } else {
211                quote! {
212                    #pattern => {}
213                }
214            }
215        }
216    }
217}
218
219fn extract_event_enum(attrs: &[Attribute]) -> syn::Path {
220    for attr in attrs {
221        if attr.path().is_ident("taskspec") {
222            if let Meta::List(meta_list) = &attr.meta {
223                let nested = match NestedMeta::parse_meta_list(meta_list.tokens.clone()) {
224                    Ok(nested) => nested,
225                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute syntax: {}", e),
226                };
227                let args = match EnumArgs::from_list(&nested) {
228                    Ok(args) => args,
229                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute: {}", e),
230                };
231                return args.event_enum;
232            }
233        }
234    }
235    abort!(
236        proc_macro2::Span::mixed_site(),
237        "Missing #[taskspec(event_enum = \"YourEventType\")] attribute on enum"
238    );
239}
240
241fn extract_callback_name(attrs: &[Attribute]) -> Option<syn::Ident> {
242    for attr in attrs {
243        if attr.path().is_ident("taskspec") {
244            if let Meta::List(meta_list) = &attr.meta {
245                let nested = match NestedMeta::parse_meta_list(meta_list.tokens.clone()) {
246                    Ok(nested) => nested,
247                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute syntax: {}", e),
248                };
249                let args = match VariantArgs::from_list(&nested) {
250                    Ok(args) => args,
251                    Err(e) => abort!(attr.span(), "Invalid taskspec attribute: {}", e),
252                };
253                return Some(args.callback);
254            }
255        }
256    }
257    None
258}
259
260#[cfg(test)]
261mod tests {}