azure_keyvault_tui_macros/
lib.rs1use 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 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 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 {}