Skip to main content

cachekit_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::quote;
4use syn::{
5    parse::{Parse, ParseStream},
6    parse_macro_input, Ident, ItemFn, LitInt, LitStr, ReturnType, Token, Type,
7};
8
9// ── Attribute argument parsing ───────────────────────────────────────────────
10
11struct MacroArgs {
12    client: Ident,
13    ttl: u64,
14    namespace: Option<String>,
15    secure: bool,
16}
17
18impl Parse for MacroArgs {
19    fn parse(input: ParseStream) -> syn::Result<Self> {
20        let mut client: Option<Ident> = None;
21        let mut ttl: Option<u64> = None;
22        let mut namespace: Option<String> = None;
23        let mut secure = false;
24
25        while !input.is_empty() {
26            let key: Ident = input.parse()?;
27            match key.to_string().as_str() {
28                "client" => {
29                    input.parse::<Token![=]>()?;
30                    client = Some(input.parse()?);
31                }
32                "ttl" => {
33                    input.parse::<Token![=]>()?;
34                    let lit: LitInt = input.parse()?;
35                    ttl = Some(lit.base10_parse()?);
36                }
37                "namespace" => {
38                    input.parse::<Token![=]>()?;
39                    let lit: LitStr = input.parse()?;
40                    namespace = Some(lit.value());
41                }
42                "secure" => {
43                    secure = true;
44                }
45                other => {
46                    return Err(syn::Error::new(
47                        key.span(),
48                        format!("unknown attribute `{other}`"),
49                    ));
50                }
51            }
52
53            // Consume trailing comma if present
54            if !input.is_empty() {
55                input.parse::<Token![,]>()?;
56            }
57        }
58
59        let client = client
60            .ok_or_else(|| input.error("`client` is required: #[cachekit(client = name, ...)]"))?;
61        let ttl = ttl.ok_or_else(|| {
62            input.error("`ttl` is required: #[cachekit(client = name, ttl = 60)]")
63        })?;
64
65        Ok(MacroArgs {
66            client,
67            ttl,
68            namespace,
69            secure,
70        })
71    }
72}
73
74// ── Return-type extraction ───────────────────────────────────────────────────
75
76/// Extract `T` from `Result<T, E>` in a function's return type.
77fn extract_ok_type(ret: &ReturnType) -> syn::Result<Type> {
78    let ty = match ret {
79        ReturnType::Type(_, ty) => ty.as_ref(),
80        ReturnType::Default => {
81            return Err(syn::Error::new_spanned(
82                ret,
83                "#[cachekit] function must return Result<T, CachekitError>",
84            ));
85        }
86    };
87
88    // Walk through the type to find Result<T, E> and extract T.
89    if let Type::Path(type_path) = ty {
90        if let Some(seg) = type_path.path.segments.last() {
91            if seg.ident != "Result" {
92                return Err(syn::Error::new_spanned(
93                    ty,
94                    "#[cachekit] function must return Result<T, CachekitError>",
95                ));
96            }
97            if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
98                if let Some(syn::GenericArgument::Type(first)) = args.args.first() {
99                    return Ok(first.clone());
100                }
101            }
102        }
103    }
104
105    Err(syn::Error::new_spanned(
106        ty,
107        "#[cachekit] function must return Result<T, CachekitError>",
108    ))
109}
110
111// ── Proc-macro entry point ──────────────────────────────────────────────────
112
113/// Cache the result of an async function.
114///
115/// # Attributes
116///
117/// - `client = <ident>` (required): Name of the `&CacheKit` parameter.
118/// - `ttl = <integer>` (required): TTL in seconds.
119/// - `namespace = "<string>"` (optional): Namespace prefix for the cache key.
120/// - `secure` (optional flag): Use encrypted cache via `cache.secure()`.
121///
122/// # Example
123///
124/// ```ignore
125/// #[cachekit(client = cache, ttl = 60)]
126/// async fn get_user(cache: &CacheKit, id: u64) -> Result<User, CachekitError> {
127///     Ok(User { name: format!("User {id}") })
128/// }
129/// ```
130#[proc_macro_attribute]
131pub fn cachekit(attr: TokenStream, item: TokenStream) -> TokenStream {
132    let args = parse_macro_input!(attr as MacroArgs);
133    let input_fn = parse_macro_input!(item as ItemFn);
134
135    match expand(&args, input_fn) {
136        Ok(tokens) => tokens.into(),
137        Err(e) => e.to_compile_error().into(),
138    }
139}
140
141fn expand(args: &MacroArgs, mut func: ItemFn) -> syn::Result<TokenStream2> {
142    let client_ident = &args.client;
143    let ttl_secs = args.ttl;
144    let namespace = args.namespace.as_deref().unwrap_or("");
145    let fn_name = func.sig.ident.to_string();
146
147    // Extract the Ok type from Result<T, E>
148    let ok_type = extract_ok_type(&func.sig.output)?;
149
150    // Collect non-client parameter idents for cache key serialization.
151    let non_client_idents: Vec<&Ident> = func
152        .sig
153        .inputs
154        .iter()
155        .filter_map(|arg| {
156            if let syn::FnArg::Typed(pat) = arg {
157                if let syn::Pat::Ident(pi) = pat.pat.as_ref() {
158                    if pi.ident != *client_ident {
159                        return Some(&pi.ident);
160                    }
161                }
162            }
163            None
164        })
165        .collect();
166
167    // Build the tuple expression for serialization: (&arg1, &arg2, ...)
168    let args_tuple = if non_client_idents.is_empty() {
169        quote! { () }
170    } else {
171        let refs = non_client_idents.iter().map(|id| quote! { &#id });
172        quote! { (#(#refs,)*) }
173    };
174
175    // Generate cache get/set calls depending on `secure` flag.
176    let (get_expr, set_expr) = if args.secure {
177        (
178            quote! {
179                {
180                    let __ck_sec = #client_ident.secure()?;
181                    __ck_sec.get::<#ok_type>(&__ck_key).await?
182                }
183            },
184            quote! {
185                let __ck_sec = #client_ident.secure()?;
186                let _ = __ck_sec.set_with_ttl(&__ck_key, __ck_val, std::time::Duration::from_secs(#ttl_secs)).await;
187            },
188        )
189    } else {
190        (
191            quote! { #client_ident.get::<#ok_type>(&__ck_key).await? },
192            quote! {
193                let _ = #client_ident.set_with_ttl(&__ck_key, __ck_val, std::time::Duration::from_secs(#ttl_secs)).await;
194            },
195        )
196    };
197
198    // Capture the original function body.
199    let original_body = &func.block;
200
201    // Extract the raw return type (without `->` arrow) for type annotation.
202    let ret_ty = match &func.sig.output {
203        ReturnType::Type(_, ty) => ty.as_ref(),
204        ReturnType::Default => {
205            return Err(syn::Error::new_spanned(
206                &func.sig,
207                "#[cachekit] function must return Result<T, CachekitError>",
208            ));
209        }
210    };
211
212    // Build the new function body.
213    let new_body: syn::Block = syn::parse_quote! {
214        {
215            // Serialize non-client args for cache key generation
216            let __ck_args = cachekit::__private::rmp_serde::to_vec(&#args_tuple)
217                .map_err(|e| cachekit::error::CachekitError::Serialization(e.to_string()))?;
218            let __ck_key = cachekit::key::generate_cache_key(#namespace, #fn_name, &__ck_args)?;
219
220            // Try cache hit
221            if let Some(__ck_cached) = #get_expr {
222                return Ok(__ck_cached);
223            }
224
225            // Execute original function body
226            let __ck_result: #ret_ty = (async #original_body).await;
227
228            // Cache on success
229            if let Ok(ref __ck_val) = __ck_result {
230                #set_expr
231            }
232
233            __ck_result
234        }
235    };
236
237    *func.block = new_body;
238    Ok(quote! { #func })
239}