Skip to main content

accelerator_macros/
lib.rs

1use proc_macro::TokenStream;
2
3use proc_macro_crate::{FoundCrate, crate_name};
4use quote::{format_ident, quote};
5use syn::parse::Parser;
6use syn::punctuated::Punctuated;
7use syn::{
8    Error, Expr, ExprLit, FnArg, GenericArgument, ItemFn, Lit, Meta, MetaNameValue, PathArguments,
9    ReturnType, Signature, Token, Type, TypePath, parse_macro_input, spanned::Spanned,
10};
11
12/// Error handling policy for cache operation failures.
13#[derive(Clone, Copy, PartialEq, Eq)]
14enum OnCacheError {
15    /// Ignore cache errors and keep business flow running.
16    Ignore,
17    /// Return cache errors to caller immediately.
18    Propagate,
19}
20
21/// Parsed arguments for `#[cacheable(...)]`.
22struct CacheableArgs {
23    cache: Expr,
24    key: Expr,
25    allow_stale: bool,
26    cache_none: bool,
27    on_cache_error: OnCacheError,
28}
29
30/// Parsed arguments for `#[cache_put(...)]`.
31struct CachePutArgs {
32    cache: Expr,
33    key: Expr,
34    value: Expr,
35    on_cache_error: OnCacheError,
36}
37
38/// Parsed arguments for `#[cache_evict(...)]`.
39struct CacheEvictArgs {
40    cache: Expr,
41    key: Expr,
42    before: bool,
43    on_cache_error: OnCacheError,
44}
45
46/// Parsed arguments for `#[cacheable_batch(...)]`.
47struct CacheableBatchArgs {
48    cache: Expr,
49    keys: Expr,
50    allow_stale: bool,
51    on_cache_error: OnCacheError,
52}
53
54/// Parsed arguments for `#[cache_evict_batch(...)]`.
55struct CacheEvictBatchArgs {
56    cache: Expr,
57    keys: Expr,
58    before: bool,
59    on_cache_error: OnCacheError,
60}
61
62/// Cache-first wrapper: hit returns directly, miss executes function and backfills cache.
63#[proc_macro_attribute]
64pub fn cacheable(attr: TokenStream, item: TokenStream) -> TokenStream {
65    let args = match parse_cacheable_args(attr) {
66        Ok(args) => args,
67        Err(err) => return err.to_compile_error().into(),
68    };
69    let input = parse_macro_input!(item as ItemFn);
70    match expand_cacheable(args, input) {
71        Ok(expanded) => expanded.into(),
72        Err(err) => err.to_compile_error().into(),
73    }
74}
75
76/// Write-through wrapper: executes function and updates cache on success.
77#[proc_macro_attribute]
78pub fn cache_put(attr: TokenStream, item: TokenStream) -> TokenStream {
79    let args = match parse_cache_put_args(attr) {
80        Ok(args) => args,
81        Err(err) => return err.to_compile_error().into(),
82    };
83    let input = parse_macro_input!(item as ItemFn);
84    match expand_cache_put(args, input) {
85        Ok(expanded) => expanded.into(),
86        Err(err) => err.to_compile_error().into(),
87    }
88}
89
90/// Invalidation wrapper: deletes cache before/after function based on `before` option.
91#[proc_macro_attribute]
92pub fn cache_evict(attr: TokenStream, item: TokenStream) -> TokenStream {
93    let args = match parse_cache_evict_args(attr) {
94        Ok(args) => args,
95        Err(err) => return err.to_compile_error().into(),
96    };
97    let input = parse_macro_input!(item as ItemFn);
98    match expand_cache_evict(args, input) {
99        Ok(expanded) => expanded.into(),
100        Err(err) => err.to_compile_error().into(),
101    }
102}
103
104/// Batch cache-first wrapper: `mget` first, then loads misses and `mset` backfill.
105#[proc_macro_attribute]
106pub fn cacheable_batch(attr: TokenStream, item: TokenStream) -> TokenStream {
107    let args = match parse_cacheable_batch_args(attr) {
108        Ok(args) => args,
109        Err(err) => return err.to_compile_error().into(),
110    };
111    let input = parse_macro_input!(item as ItemFn);
112    match expand_cacheable_batch(args, input) {
113        Ok(expanded) => expanded.into(),
114        Err(err) => err.to_compile_error().into(),
115    }
116}
117
118/// Batch invalidation wrapper: `mdel` before/after function according to `before`.
119#[proc_macro_attribute]
120pub fn cache_evict_batch(attr: TokenStream, item: TokenStream) -> TokenStream {
121    let args = match parse_cache_evict_batch_args(attr) {
122        Ok(args) => args,
123        Err(err) => return err.to_compile_error().into(),
124    };
125    let input = parse_macro_input!(item as ItemFn);
126    match expand_cache_evict_batch(args, input) {
127        Ok(expanded) => expanded.into(),
128        Err(err) => err.to_compile_error().into(),
129    }
130}
131
132fn expand_cacheable(
133    args: CacheableArgs,
134    mut item: ItemFn,
135) -> syn::Result<proc_macro2::TokenStream> {
136    ensure_async_method(&item.sig, "cacheable")?;
137    let ok_ty = parse_ok_type(&item.sig, "cacheable")?;
138    ensure_option_type(
139        &ok_ty,
140        "cacheable requires a result-like return type where Ok is `Option<T>`",
141    )?;
142
143    let runtime = runtime_crate_path();
144    let on_get_error = on_cache_error_tokens(args.on_cache_error, &runtime, "cacheable", "get");
145    let on_set_error = on_cache_error_tokens(args.on_cache_error, &runtime, "cacheable", "set");
146    let cache_expr = args.cache;
147    let key_expr = args.key;
148    let allow_stale = args.allow_stale;
149    let cache_none = args.cache_none;
150    let original_block = item.block;
151
152    item.block = Box::new(syn::parse_quote!({
153        let __key = #key_expr;
154        let __opts = #runtime::ReadOptions {
155            allow_stale: #allow_stale,
156            disable_load: true,
157        };
158
159        match (#cache_expr).get(&__key, &__opts).await {
160            Ok(Some(__hit)) => return Ok(Some(__hit)),
161            Ok(None) => {}
162            Err(__cache_err) => {
163                #on_get_error
164            }
165        }
166
167        let __result = (async #original_block).await;
168        match __result {
169            Ok(__value) => {
170                if let Some(__present) = __value.as_ref() {
171                    if let Err(__cache_err) = (#cache_expr).set(&__key, Some(__present.clone())).await {
172                        #on_set_error
173                    }
174                } else if #cache_none {
175                    if let Err(__cache_err) = (#cache_expr).set(&__key, None).await {
176                        #on_set_error
177                    }
178                }
179                Ok(__value)
180            }
181            Err(__err) => Err(__err),
182        }
183    }));
184
185    Ok(quote!(#item))
186}
187
188fn expand_cache_put(args: CachePutArgs, mut item: ItemFn) -> syn::Result<proc_macro2::TokenStream> {
189    ensure_async_method(&item.sig, "cache_put")?;
190    parse_ok_type(&item.sig, "cache_put")?;
191
192    let runtime = runtime_crate_path();
193    let on_set_error = on_cache_error_tokens(args.on_cache_error, &runtime, "cache_put", "set");
194    let cache_expr = args.cache;
195    let key_expr = args.key;
196    let value_expr = args.value;
197    let original_block = item.block;
198
199    item.block = Box::new(syn::parse_quote!({
200        let __key = #key_expr;
201
202        let __result = (async #original_block).await;
203        match __result {
204            Ok(__ok) => {
205                let __value = #value_expr;
206                if let Err(__cache_err) = (#cache_expr).set(&__key, __value).await {
207                    #on_set_error
208                }
209                Ok(__ok)
210            }
211            Err(__err) => Err(__err),
212        }
213    }));
214
215    Ok(quote!(#item))
216}
217
218fn expand_cache_evict(
219    args: CacheEvictArgs,
220    mut item: ItemFn,
221) -> syn::Result<proc_macro2::TokenStream> {
222    ensure_async_method(&item.sig, "cache_evict")?;
223    parse_ok_type(&item.sig, "cache_evict")?;
224
225    let runtime = runtime_crate_path();
226    let before = args.before;
227    let on_before_error =
228        on_cache_error_tokens(args.on_cache_error, &runtime, "cache_evict", "del_before");
229    let on_after_error =
230        on_cache_error_tokens(args.on_cache_error, &runtime, "cache_evict", "del_after");
231    let cache_expr = args.cache;
232    let key_expr = args.key;
233    let original_block = item.block;
234
235    item.block = Box::new(syn::parse_quote!({
236        let __key = #key_expr;
237
238        if #before {
239            if let Err(__cache_err) = (#cache_expr).del(&__key).await {
240                #on_before_error
241            }
242        }
243
244        let __result = (async #original_block).await;
245        match __result {
246            Ok(__ok) => {
247                if !#before {
248                    if let Err(__cache_err) = (#cache_expr).del(&__key).await {
249                        #on_after_error
250                    }
251                }
252                Ok(__ok)
253            }
254            Err(__err) => Err(__err),
255        }
256    }));
257
258    Ok(quote!(#item))
259}
260
261fn expand_cacheable_batch(
262    args: CacheableBatchArgs,
263    mut item: ItemFn,
264) -> syn::Result<proc_macro2::TokenStream> {
265    ensure_async_method(&item.sig, "cacheable_batch")?;
266    let ok_ty = parse_ok_type(&item.sig, "cacheable_batch")?;
267    ensure_hashmap_option_type(
268        &ok_ty,
269        "cacheable_batch requires a result-like return type where Ok is `HashMap<K, Option<V>>`",
270    )?;
271
272    let runtime = runtime_crate_path();
273    let on_mget_error = on_cache_error_with_fallback_tokens(
274        args.on_cache_error,
275        &runtime,
276        "cacheable_batch",
277        "mget",
278        quote!(::std::collections::HashMap::new()),
279    );
280    let on_mset_error =
281        on_cache_error_tokens(args.on_cache_error, &runtime, "cacheable_batch", "mset");
282    let cache_expr = args.cache;
283    let keys_expr = args.keys;
284    let allow_stale = args.allow_stale;
285    let miss_keys_rebind = build_miss_keys_rebind(&item.sig, &keys_expr, "cacheable_batch")?;
286    let original_block = item.block;
287
288    item.block = Box::new(syn::parse_quote!({
289        let __requested = {
290            let mut keys = ::std::vec::Vec::new();
291            let mut seen = ::std::collections::HashSet::new();
292            for key in (#keys_expr).iter().cloned() {
293                if seen.insert(key.clone()) {
294                    keys.push(key);
295                }
296            }
297            keys
298        };
299
300        if __requested.is_empty() {
301            return Ok(::std::collections::HashMap::new());
302        }
303
304        let __opts = #runtime::ReadOptions {
305            allow_stale: #allow_stale,
306            disable_load: true,
307        };
308
309        let mut __values = match (#cache_expr).mget(&__requested, &__opts).await {
310            Ok(cached) => cached,
311            Err(__cache_err) => {
312                #on_mget_error
313            }
314        };
315
316        let __misses = __requested
317            .iter()
318            .filter(|key| match __values.get(*key) {
319                Some(value) => value.is_none(),
320                None => true,
321            })
322            .cloned()
323            .collect::<::std::vec::Vec<_>>();
324
325        if __misses.is_empty() {
326            return Ok(__values);
327        }
328
329        #miss_keys_rebind
330
331        let __result = (async #original_block).await;
332        match __result {
333            Ok(__loaded_map) => {
334                let mut __writes = ::std::collections::HashMap::with_capacity(__misses.len());
335                for key in __misses {
336                    let loaded = __loaded_map.get(&key).cloned().unwrap_or(None);
337                    __values.insert(key.clone(), loaded.clone());
338                    __writes.insert(key, loaded);
339                }
340
341                if !__writes.is_empty() {
342                    if let Err(__cache_err) = (#cache_expr).mset(__writes).await {
343                        #on_mset_error
344                    }
345                }
346
347                Ok(__values)
348            }
349            Err(__err) => Err(__err),
350        }
351    }));
352
353    Ok(quote!(#item))
354}
355
356fn expand_cache_evict_batch(
357    args: CacheEvictBatchArgs,
358    mut item: ItemFn,
359) -> syn::Result<proc_macro2::TokenStream> {
360    ensure_async_method(&item.sig, "cache_evict_batch")?;
361    parse_ok_type(&item.sig, "cache_evict_batch")?;
362
363    let runtime = runtime_crate_path();
364    let before = args.before;
365    let on_before_error = on_cache_error_tokens(
366        args.on_cache_error,
367        &runtime,
368        "cache_evict_batch",
369        "mdel_before",
370    );
371    let on_after_error = on_cache_error_tokens(
372        args.on_cache_error,
373        &runtime,
374        "cache_evict_batch",
375        "mdel_after",
376    );
377    let cache_expr = args.cache;
378    let keys_expr = args.keys;
379    let original_block = item.block;
380
381    item.block = Box::new(syn::parse_quote!({
382        let __keys = {
383            let mut keys = ::std::vec::Vec::new();
384            let mut seen = ::std::collections::HashSet::new();
385            for key in (#keys_expr).iter().cloned() {
386                if seen.insert(key.clone()) {
387                    keys.push(key);
388                }
389            }
390            keys
391        };
392
393        if #before && !__keys.is_empty() {
394            if let Err(__cache_err) = (#cache_expr).mdel(&__keys).await {
395                #on_before_error
396            }
397        }
398
399        let __result = (async #original_block).await;
400        match __result {
401            Ok(__ok) => {
402                if !#before && !__keys.is_empty() {
403                    if let Err(__cache_err) = (#cache_expr).mdel(&__keys).await {
404                        #on_after_error
405                    }
406                }
407                Ok(__ok)
408            }
409            Err(__err) => Err(__err),
410        }
411    }));
412
413    Ok(quote!(#item))
414}
415
416/// Ensures the macro is used on an async method with a receiver.
417fn ensure_async_method(sig: &Signature, macro_name: &str) -> syn::Result<()> {
418    if sig.asyncness.is_none() {
419        return Err(Error::new(
420            sig.span(),
421            format!(
422                "{macro_name} only supports async methods, expected `async fn ...(&self, ...)`"
423            ),
424        ));
425    }
426
427    let Some(first) = sig.inputs.first() else {
428        return Err(Error::new(
429            sig.span(),
430            format!(
431                "{macro_name} requires a method receiver, expected first argument `&self` or `&mut self`"
432            ),
433        ));
434    };
435
436    if !matches!(first, FnArg::Receiver(_)) {
437        return Err(Error::new(
438            first.span(),
439            format!(
440                "{macro_name} only supports methods on impl blocks, expected first argument `&self` or `&mut self`"
441            ),
442        ));
443    }
444
445    Ok(())
446}
447
448/// Parses and validates result-like return type, then extracts the first generic type argument.
449fn parse_ok_type(sig: &Signature, macro_name: &str) -> syn::Result<Type> {
450    let ReturnType::Type(_, ty) = &sig.output else {
451        return Err(Error::new(
452            sig.span(),
453            format!("{macro_name} requires a result-like return type"),
454        ));
455    };
456
457    let Type::Path(TypePath { path, .. }) = ty.as_ref() else {
458        return Err(Error::new(
459            ty.span(),
460            format!("{macro_name} requires a result-like return type"),
461        ));
462    };
463
464    let Some(last) = path.segments.last() else {
465        return Err(Error::new(
466            path.span(),
467            format!("{macro_name} requires a result-like return type"),
468        ));
469    };
470
471    let PathArguments::AngleBracketed(args) = &last.arguments else {
472        return Err(Error::new(
473            last.arguments.span(),
474            format!("{macro_name} requires a result-like return type"),
475        ));
476    };
477
478    let mut type_args = args.args.iter().filter_map(|arg| {
479        if let GenericArgument::Type(ty) = arg {
480            Some(ty.clone())
481        } else {
482            None
483        }
484    });
485
486    let Some(ok) = type_args.next() else {
487        return Err(Error::new(
488            args.span(),
489            format!("{macro_name} requires a result-like return type"),
490        ));
491    };
492
493    Ok(ok)
494}
495
496/// Ensures a type is `Option<T>`.
497fn ensure_option_type(ty: &Type, message: &str) -> syn::Result<()> {
498    let Type::Path(TypePath { path, .. }) = ty else {
499        return Err(Error::new(ty.span(), message));
500    };
501
502    let Some(last) = path.segments.last() else {
503        return Err(Error::new(path.span(), message));
504    };
505
506    if last.ident != "Option" {
507        return Err(Error::new(last.ident.span(), message));
508    }
509
510    Ok(())
511}
512
513/// Ensures a type is `HashMap<K, Option<V>>`.
514fn ensure_hashmap_option_type(ty: &Type, message: &str) -> syn::Result<()> {
515    let Type::Path(TypePath { path, .. }) = ty else {
516        return Err(Error::new(ty.span(), message));
517    };
518
519    let Some(last) = path.segments.last() else {
520        return Err(Error::new(path.span(), message));
521    };
522
523    if last.ident != "HashMap" {
524        return Err(Error::new(last.ident.span(), message));
525    }
526
527    let PathArguments::AngleBracketed(args) = &last.arguments else {
528        return Err(Error::new(last.arguments.span(), message));
529    };
530
531    let mut type_args = args.args.iter().filter_map(|arg| {
532        if let GenericArgument::Type(ty) = arg {
533            Some(ty)
534        } else {
535            None
536        }
537    });
538
539    let _key_ty = type_args.next();
540    let Some(value_ty) = type_args.next() else {
541        return Err(Error::new(args.span(), message));
542    };
543
544    ensure_option_type(value_ty, message)
545}
546
547/// Rebinds `keys` method parameter to cache misses when possible.
548fn build_miss_keys_rebind(
549    sig: &Signature,
550    keys_expr: &Expr,
551    macro_name: &str,
552) -> syn::Result<proc_macro2::TokenStream> {
553    let Expr::Path(path) = keys_expr else {
554        return Ok(quote! {});
555    };
556
557    let Some(keys_ident) = path.path.get_ident() else {
558        return Ok(quote! {});
559    };
560
561    let Some(keys_ty) = method_arg_type(sig, keys_ident) else {
562        return Ok(quote! {});
563    };
564
565    match keys_ty {
566        Type::Path(TypePath { path, .. }) => {
567            let Some(last) = path.segments.last() else {
568                return Ok(quote! {});
569            };
570
571            if last.ident == "Vec" {
572                return Ok(quote! {
573                    let #keys_ident = __misses.clone();
574                });
575            }
576        }
577        Type::Reference(reference) => match reference.elem.as_ref() {
578            Type::Slice(_) => {
579                return Ok(quote! {
580                    let #keys_ident = __misses.as_slice();
581                });
582            }
583            Type::Path(TypePath { path, .. }) => {
584                let Some(last) = path.segments.last() else {
585                    return Ok(quote! {});
586                };
587
588                if last.ident == "Vec" {
589                    return Ok(quote! {
590                        let #keys_ident = &__misses;
591                    });
592                }
593            }
594            _ => {}
595        },
596        _ => {}
597    }
598
599    Err(Error::new(
600        keys_expr.span(),
601        format!(
602            "{macro_name} only supports `keys` parameter types `Vec<K>`, `&[K]` or `&Vec<K>` when reusing misses in function body"
603        ),
604    ))
605}
606
607/// Finds method argument type by identifier name.
608fn method_arg_type(sig: &Signature, ident: &syn::Ident) -> Option<Type> {
609    for arg in &sig.inputs {
610        let FnArg::Typed(typed) = arg else {
611            continue;
612        };
613
614        let syn::Pat::Ident(pat_ident) = typed.pat.as_ref() else {
615            continue;
616        };
617
618        if pat_ident.ident == *ident {
619            return Some((*typed.ty).clone());
620        }
621    }
622
623    None
624}
625
626/// Generates error handling branch for cache operation failures.
627fn on_cache_error_tokens(
628    mode: OnCacheError,
629    runtime: &proc_macro2::TokenStream,
630    macro_name: &str,
631    op_name: &str,
632) -> proc_macro2::TokenStream {
633    match mode {
634        OnCacheError::Ignore => quote! {
635            #runtime::tracing::warn!(
636                target: "accelerator::macros",
637                cache_macro = #macro_name,
638                op = #op_name,
639                result = "ignored",
640                error = %__cache_err,
641                "cache operation failed, continue business flow"
642            );
643        },
644        OnCacheError::Propagate => quote! {
645            #runtime::tracing::warn!(
646                target: "accelerator::macros",
647                cache_macro = #macro_name,
648                op = #op_name,
649                result = "propagated",
650                error = %__cache_err,
651                "cache operation failed, return error"
652            );
653            return Err(::core::convert::Into::into(__cache_err));
654        },
655    }
656}
657
658/// Generates error handling branch for cache operation failures with expression fallback.
659fn on_cache_error_with_fallback_tokens(
660    mode: OnCacheError,
661    runtime: &proc_macro2::TokenStream,
662    macro_name: &str,
663    op_name: &str,
664    fallback: proc_macro2::TokenStream,
665) -> proc_macro2::TokenStream {
666    match mode {
667        OnCacheError::Ignore => quote! {
668            #runtime::tracing::warn!(
669                target: "accelerator::macros",
670                cache_macro = #macro_name,
671                op = #op_name,
672                result = "ignored",
673                error = %__cache_err,
674                "cache operation failed, continue business flow"
675            );
676            #fallback
677        },
678        OnCacheError::Propagate => quote! {
679            #runtime::tracing::warn!(
680                target: "accelerator::macros",
681                cache_macro = #macro_name,
682                op = #op_name,
683                result = "propagated",
684                error = %__cache_err,
685                "cache operation failed, return error"
686            );
687            return Err(::core::convert::Into::into(__cache_err));
688        },
689    }
690}
691
692/// Parses `cacheable` attribute arguments.
693fn parse_cacheable_args(attr: TokenStream) -> syn::Result<CacheableArgs> {
694    let metas = parse_attr_metas(attr)?;
695
696    let mut cache = None;
697    let mut key = None;
698    let mut allow_stale = None;
699    let mut cache_none = None;
700    let mut on_cache_error = None;
701
702    for meta in metas {
703        let (name, value, span) = parse_name_value(meta)?;
704        match name.as_str() {
705            "cache" => set_once(&mut cache, value, "cache", span)?,
706            "key" => set_once(&mut key, value, "key", span)?,
707            "allow_stale" => set_once(
708                &mut allow_stale,
709                parse_bool_expr(&value, span)?,
710                "allow_stale",
711                span,
712            )?,
713            "cache_none" => set_once(
714                &mut cache_none,
715                parse_bool_expr(&value, span)?,
716                "cache_none",
717                span,
718            )?,
719            "on_cache_error" => set_once(
720                &mut on_cache_error,
721                parse_on_cache_error(&value, span)?,
722                "on_cache_error",
723                span,
724            )?,
725            _ => {
726                return Err(Error::new(
727                    span,
728                    "unknown cacheable argument, supported: cache, key, allow_stale, cache_none, on_cache_error",
729                ));
730            }
731        }
732    }
733
734    Ok(CacheableArgs {
735        cache: required(cache, "cache")?,
736        key: required(key, "key")?,
737        allow_stale: allow_stale.unwrap_or(false),
738        cache_none: cache_none.unwrap_or(true),
739        on_cache_error: on_cache_error.unwrap_or(OnCacheError::Ignore),
740    })
741}
742
743/// Parses `cache_put` attribute arguments.
744fn parse_cache_put_args(attr: TokenStream) -> syn::Result<CachePutArgs> {
745    let metas = parse_attr_metas(attr)?;
746
747    let mut cache = None;
748    let mut key = None;
749    let mut value = None;
750    let mut on_cache_error = None;
751
752    for meta in metas {
753        let (name, expr, span) = parse_name_value(meta)?;
754        match name.as_str() {
755            "cache" => set_once(&mut cache, expr, "cache", span)?,
756            "key" => set_once(&mut key, expr, "key", span)?,
757            "value" => set_once(&mut value, expr, "value", span)?,
758            "on_cache_error" => set_once(
759                &mut on_cache_error,
760                parse_on_cache_error(&expr, span)?,
761                "on_cache_error",
762                span,
763            )?,
764            _ => {
765                return Err(Error::new(
766                    span,
767                    "unknown cache_put argument, supported: cache, key, value, on_cache_error",
768                ));
769            }
770        }
771    }
772
773    Ok(CachePutArgs {
774        cache: required(cache, "cache")?,
775        key: required(key, "key")?,
776        value: required(value, "value")?,
777        on_cache_error: on_cache_error.unwrap_or(OnCacheError::Ignore),
778    })
779}
780
781/// Parses `cache_evict` attribute arguments.
782fn parse_cache_evict_args(attr: TokenStream) -> syn::Result<CacheEvictArgs> {
783    let metas = parse_attr_metas(attr)?;
784
785    let mut cache = None;
786    let mut key = None;
787    let mut before = None;
788    let mut on_cache_error = None;
789
790    for meta in metas {
791        let (name, value, span) = parse_name_value(meta)?;
792        match name.as_str() {
793            "cache" => set_once(&mut cache, value, "cache", span)?,
794            "key" => set_once(&mut key, value, "key", span)?,
795            "before" => set_once(&mut before, parse_bool_expr(&value, span)?, "before", span)?,
796            "on_cache_error" => set_once(
797                &mut on_cache_error,
798                parse_on_cache_error(&value, span)?,
799                "on_cache_error",
800                span,
801            )?,
802            _ => {
803                return Err(Error::new(
804                    span,
805                    "unknown cache_evict argument, supported: cache, key, before, on_cache_error",
806                ));
807            }
808        }
809    }
810
811    Ok(CacheEvictArgs {
812        cache: required(cache, "cache")?,
813        key: required(key, "key")?,
814        before: before.unwrap_or(false),
815        on_cache_error: on_cache_error.unwrap_or(OnCacheError::Ignore),
816    })
817}
818
819/// Parses `cacheable_batch` attribute arguments.
820fn parse_cacheable_batch_args(attr: TokenStream) -> syn::Result<CacheableBatchArgs> {
821    let metas = parse_attr_metas(attr)?;
822
823    let mut cache = None;
824    let mut keys = None;
825    let mut allow_stale = None;
826    let mut on_cache_error = None;
827
828    for meta in metas {
829        let (name, value, span) = parse_name_value(meta)?;
830        match name.as_str() {
831            "cache" => set_once(&mut cache, value, "cache", span)?,
832            "keys" => set_once(&mut keys, value, "keys", span)?,
833            "allow_stale" => set_once(
834                &mut allow_stale,
835                parse_bool_expr(&value, span)?,
836                "allow_stale",
837                span,
838            )?,
839            "on_cache_error" => set_once(
840                &mut on_cache_error,
841                parse_on_cache_error(&value, span)?,
842                "on_cache_error",
843                span,
844            )?,
845            _ => {
846                return Err(Error::new(
847                    span,
848                    "unknown cacheable_batch argument, supported: cache, keys, allow_stale, on_cache_error",
849                ));
850            }
851        }
852    }
853
854    Ok(CacheableBatchArgs {
855        cache: required(cache, "cache")?,
856        keys: required(keys, "keys")?,
857        allow_stale: allow_stale.unwrap_or(false),
858        on_cache_error: on_cache_error.unwrap_or(OnCacheError::Ignore),
859    })
860}
861
862/// Parses `cache_evict_batch` attribute arguments.
863fn parse_cache_evict_batch_args(attr: TokenStream) -> syn::Result<CacheEvictBatchArgs> {
864    let metas = parse_attr_metas(attr)?;
865
866    let mut cache = None;
867    let mut keys = None;
868    let mut before = None;
869    let mut on_cache_error = None;
870
871    for meta in metas {
872        let (name, value, span) = parse_name_value(meta)?;
873        match name.as_str() {
874            "cache" => set_once(&mut cache, value, "cache", span)?,
875            "keys" => set_once(&mut keys, value, "keys", span)?,
876            "before" => set_once(&mut before, parse_bool_expr(&value, span)?, "before", span)?,
877            "on_cache_error" => set_once(
878                &mut on_cache_error,
879                parse_on_cache_error(&value, span)?,
880                "on_cache_error",
881                span,
882            )?,
883            _ => {
884                return Err(Error::new(
885                    span,
886                    "unknown cache_evict_batch argument, supported: cache, keys, before, on_cache_error",
887                ));
888            }
889        }
890    }
891
892    Ok(CacheEvictBatchArgs {
893        cache: required(cache, "cache")?,
894        keys: required(keys, "keys")?,
895        before: before.unwrap_or(false),
896        on_cache_error: on_cache_error.unwrap_or(OnCacheError::Ignore),
897    })
898}
899
900/// Parses comma-separated `name = value` metadata list.
901fn parse_attr_metas(attr: TokenStream) -> syn::Result<Vec<Meta>> {
902    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
903    let metas = parser.parse(attr)?;
904    Ok(metas.into_iter().collect())
905}
906
907/// Parses one `name = value` pair.
908fn parse_name_value(meta: Meta) -> syn::Result<(String, Expr, proc_macro2::Span)> {
909    let span = meta.span();
910    let Meta::NameValue(MetaNameValue { path, value, .. }) = meta else {
911        return Err(Error::new(span, "expected `name = value`"));
912    };
913
914    let Some(ident) = path.get_ident() else {
915        return Err(Error::new(path.span(), "expected simple identifier"));
916    };
917
918    Ok((ident.to_string(), value, ident.span()))
919}
920
921/// Parses a boolean literal argument.
922fn parse_bool_expr(expr: &Expr, span: proc_macro2::Span) -> syn::Result<bool> {
923    let Expr::Lit(ExprLit {
924        lit: Lit::Bool(value),
925        ..
926    }) = expr
927    else {
928        return Err(Error::new(span, "expected boolean literal"));
929    };
930    Ok(value.value())
931}
932
933/// Parses `on_cache_error` enum value.
934fn parse_on_cache_error(expr: &Expr, span: proc_macro2::Span) -> syn::Result<OnCacheError> {
935    let Expr::Lit(ExprLit {
936        lit: Lit::Str(value),
937        ..
938    }) = expr
939    else {
940        return Err(Error::new(span, "expected string literal"));
941    };
942
943    match value.value().as_str() {
944        "ignore" => Ok(OnCacheError::Ignore),
945        "propagate" => Ok(OnCacheError::Propagate),
946        _ => Err(Error::new(
947            span,
948            "on_cache_error must be \"ignore\" or \"propagate\"",
949        )),
950    }
951}
952
953/// Ensures a field is only assigned once in attribute arguments.
954fn set_once<T>(
955    slot: &mut Option<T>,
956    value: T,
957    name: &str,
958    span: proc_macro2::Span,
959) -> syn::Result<()> {
960    if slot.is_some() {
961        return Err(Error::new(span, format!("duplicate `{name}` argument")));
962    }
963    *slot = Some(value);
964    Ok(())
965}
966
967/// Extracts required argument value.
968fn required<T>(slot: Option<T>, name: &str) -> syn::Result<T> {
969    slot.ok_or_else(|| Error::new(proc_macro2::Span::call_site(), format!("missing `{name}`")))
970}
971
972/// Resolves runtime crate path so macros work both inside and outside `accelerator`.
973fn runtime_crate_path() -> proc_macro2::TokenStream {
974    match crate_name("accelerator") {
975        Ok(FoundCrate::Itself) => quote!(::accelerator),
976        Ok(FoundCrate::Name(name)) => {
977            let ident = format_ident!("{}", name);
978            quote!(::#ident)
979        }
980        Err(_) => quote!(::accelerator),
981    }
982}