cachelito_macro_utils/
lib.rs

1//! Shared utilities for cachelito procedural macros
2//!
3//! This crate provides common parsing and code generation utilities
4//! used by both `cachelito-macros` and `cachelito-async-macros`.
5
6use proc_macro2::TokenStream as TokenStream2;
7use quote::quote;
8use syn::{punctuated::Punctuated, Expr, MetaNameValue, Token};
9
10/// List of supported eviction policies
11static POLICIES: &[&str] = &["fifo", "lru", "lfu", "arc", "random", "tlru", "w_tinylfu"];
12
13/// Helper struct to group common attributes and avoid excessive function parameters
14#[derive(Default)]
15pub struct CommonAttributes {
16    pub custom_name: Option<String>,
17    pub max_memory: TokenStream2,
18    pub tags: Vec<String>,
19    pub events: Vec<String>,
20    pub dependencies: Vec<String>,
21    pub invalidate_on: Option<syn::Path>,
22    pub cache_if: Option<syn::Path>,
23    pub frequency_weight: TokenStream2,
24    pub window_ratio: TokenStream2,
25    pub sketch_width: TokenStream2,
26    pub sketch_depth: TokenStream2,
27    pub decay_interval: TokenStream2,
28}
29
30impl CommonAttributes {
31    /// Creates a new CommonAttributes with default token streams
32    pub fn new() -> Self {
33        Self {
34            custom_name: None,
35            max_memory: quote! { None },
36            tags: Vec::new(),
37            events: Vec::new(),
38            dependencies: Vec::new(),
39            invalidate_on: None,
40            cache_if: None,
41            frequency_weight: quote! { None },
42            window_ratio: quote! { None },
43            sketch_width: quote! { None },
44            sketch_depth: quote! { None },
45            decay_interval: quote! { None },
46        }
47    }
48}
49
50pub fn policies_str_with_separator(separator: &str) -> String {
51    POLICIES
52        .iter()
53        .map(|p| format!("\"{}\"", p))
54        .collect::<Vec<_>>()
55        .join(separator)
56}
57
58/// Parsed macro attributes for async caching
59pub struct AsyncCacheAttributes {
60    pub limit: TokenStream2,
61    pub policy: TokenStream2,
62    pub ttl: TokenStream2,
63    pub custom_name: Option<String>,
64    pub max_memory: TokenStream2,
65    pub tags: Vec<String>,
66    pub events: Vec<String>,
67    pub dependencies: Vec<String>,
68    pub invalidate_on: Option<syn::Path>,
69    pub cache_if: Option<syn::Path>,
70    pub frequency_weight: TokenStream2,
71    pub window_ratio: TokenStream2,
72    pub sketch_width: TokenStream2,
73    pub sketch_depth: TokenStream2,
74    pub decay_interval: TokenStream2,
75}
76
77impl Default for AsyncCacheAttributes {
78    fn default() -> Self {
79        Self {
80            limit: quote! { Option::<usize>::None },
81            policy: quote! { "fifo" },
82            ttl: quote! { Option::<u64>::None },
83            custom_name: None,
84            max_memory: quote! { Option::<usize>::None },
85            tags: Vec::new(),
86            events: Vec::new(),
87            dependencies: Vec::new(),
88            invalidate_on: None,
89            cache_if: None,
90            frequency_weight: quote! { Option::<f64>::None },
91            window_ratio: quote! { Option::<f64>::None },
92            sketch_width: quote! { Option::<usize>::None },
93            sketch_depth: quote! { Option::<usize>::None },
94            decay_interval: quote! { Option::<u64>::None },
95        }
96    }
97}
98
99impl AsyncCacheAttributes {
100    /// Create a new builder for AsyncCacheAttributes
101    pub fn builder() -> AsyncCacheAttributesBuilder {
102        AsyncCacheAttributesBuilder::new()
103    }
104}
105
106/// Builder for AsyncCacheAttributes following the Builder pattern
107pub struct AsyncCacheAttributesBuilder {
108    attrs: AsyncCacheAttributes,
109}
110
111impl AsyncCacheAttributesBuilder {
112    pub fn new() -> Self {
113        Self {
114            attrs: AsyncCacheAttributes::default(),
115        }
116    }
117
118    pub fn limit(mut self, limit: TokenStream2) -> Self {
119        self.attrs.limit = limit;
120        self
121    }
122
123    pub fn policy(mut self, policy: TokenStream2) -> Self {
124        self.attrs.policy = policy;
125        self
126    }
127
128    pub fn ttl(mut self, ttl: TokenStream2) -> Self {
129        self.attrs.ttl = ttl;
130        self
131    }
132
133    pub fn custom_name(mut self, name: Option<String>) -> Self {
134        self.attrs.custom_name = name;
135        self
136    }
137
138    pub fn max_memory(mut self, max_memory: TokenStream2) -> Self {
139        self.attrs.max_memory = max_memory;
140        self
141    }
142
143    pub fn tags(mut self, tags: Vec<String>) -> Self {
144        self.attrs.tags = tags;
145        self
146    }
147
148    pub fn events(mut self, events: Vec<String>) -> Self {
149        self.attrs.events = events;
150        self
151    }
152
153    pub fn dependencies(mut self, dependencies: Vec<String>) -> Self {
154        self.attrs.dependencies = dependencies;
155        self
156    }
157
158    pub fn invalidate_on(mut self, invalidate_on: Option<syn::Path>) -> Self {
159        self.attrs.invalidate_on = invalidate_on;
160        self
161    }
162
163    pub fn cache_if(mut self, cache_if: Option<syn::Path>) -> Self {
164        self.attrs.cache_if = cache_if;
165        self
166    }
167
168    pub fn frequency_weight(mut self, frequency_weight: TokenStream2) -> Self {
169        self.attrs.frequency_weight = frequency_weight;
170        self
171    }
172
173    pub fn window_ratio(mut self, window_ratio: TokenStream2) -> Self {
174        self.attrs.window_ratio = window_ratio;
175        self
176    }
177
178    pub fn sketch_width(mut self, sketch_width: TokenStream2) -> Self {
179        self.attrs.sketch_width = sketch_width;
180        self
181    }
182
183    pub fn sketch_depth(mut self, sketch_depth: TokenStream2) -> Self {
184        self.attrs.sketch_depth = sketch_depth;
185        self
186    }
187
188    pub fn decay_interval(mut self, decay_interval: TokenStream2) -> Self {
189        self.attrs.decay_interval = decay_interval;
190        self
191    }
192
193    /// Apply common attributes from CommonAttributes struct
194    pub fn with_common(mut self, common: CommonAttributes) -> Self {
195        self.attrs.custom_name = common.custom_name;
196        self.attrs.max_memory = common.max_memory;
197        self.attrs.tags = common.tags;
198        self.attrs.events = common.events;
199        self.attrs.dependencies = common.dependencies;
200        self.attrs.invalidate_on = common.invalidate_on;
201        self.attrs.cache_if = common.cache_if;
202        self.attrs.frequency_weight = common.frequency_weight;
203        self.attrs.window_ratio = common.window_ratio;
204        self.attrs.sketch_width = common.sketch_width;
205        self.attrs.sketch_depth = common.sketch_depth;
206        self.attrs.decay_interval = common.decay_interval;
207        self
208    }
209
210    pub fn build(self) -> AsyncCacheAttributes {
211        self.attrs
212    }
213}
214
215impl Default for AsyncCacheAttributesBuilder {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221/// Parsed macro attributes for sync caching
222pub struct SyncCacheAttributes {
223    pub limit: TokenStream2,
224    pub policy: TokenStream2,
225    pub ttl: TokenStream2,
226    pub scope: TokenStream2,
227    pub custom_name: Option<String>,
228    pub max_memory: TokenStream2,
229    pub tags: Vec<String>,
230    pub events: Vec<String>,
231    pub dependencies: Vec<String>,
232    pub invalidate_on: Option<syn::Path>,
233    pub cache_if: Option<syn::Path>,
234    pub frequency_weight: TokenStream2,
235    pub window_ratio: TokenStream2,
236    pub sketch_width: TokenStream2,
237    pub sketch_depth: TokenStream2,
238    pub decay_interval: TokenStream2,
239}
240
241impl Default for SyncCacheAttributes {
242    fn default() -> Self {
243        Self {
244            limit: quote! { None },
245            policy: quote! { cachelito_core::EvictionPolicy::FIFO },
246            ttl: quote! { None },
247            scope: quote! { cachelito_core::CacheScope::Global },
248            custom_name: None,
249            max_memory: quote! { None },
250            tags: Vec::new(),
251            events: Vec::new(),
252            dependencies: Vec::new(),
253            invalidate_on: None,
254            cache_if: None,
255            frequency_weight: quote! { None },
256            window_ratio: quote! { None },
257            sketch_width: quote! { None },
258            sketch_depth: quote! { None },
259            decay_interval: quote! { None },
260        }
261    }
262}
263
264impl SyncCacheAttributes {
265    /// Create a new builder for SyncCacheAttributes
266    pub fn builder() -> SyncCacheAttributesBuilder {
267        SyncCacheAttributesBuilder::new()
268    }
269}
270
271/// Builder for SyncCacheAttributes following the Builder pattern
272pub struct SyncCacheAttributesBuilder {
273    attrs: SyncCacheAttributes,
274}
275
276impl SyncCacheAttributesBuilder {
277    pub fn new() -> Self {
278        Self {
279            attrs: SyncCacheAttributes::default(),
280        }
281    }
282
283    pub fn limit(mut self, limit: TokenStream2) -> Self {
284        self.attrs.limit = limit;
285        self
286    }
287
288    pub fn policy(mut self, policy: TokenStream2) -> Self {
289        self.attrs.policy = policy;
290        self
291    }
292
293    pub fn ttl(mut self, ttl: TokenStream2) -> Self {
294        self.attrs.ttl = ttl;
295        self
296    }
297
298    pub fn scope(mut self, scope: TokenStream2) -> Self {
299        self.attrs.scope = scope;
300        self
301    }
302
303    pub fn custom_name(mut self, name: Option<String>) -> Self {
304        self.attrs.custom_name = name;
305        self
306    }
307
308    pub fn max_memory(mut self, max_memory: TokenStream2) -> Self {
309        self.attrs.max_memory = max_memory;
310        self
311    }
312
313    pub fn tags(mut self, tags: Vec<String>) -> Self {
314        self.attrs.tags = tags;
315        self
316    }
317
318    pub fn events(mut self, events: Vec<String>) -> Self {
319        self.attrs.events = events;
320        self
321    }
322
323    pub fn dependencies(mut self, dependencies: Vec<String>) -> Self {
324        self.attrs.dependencies = dependencies;
325        self
326    }
327
328    pub fn invalidate_on(mut self, invalidate_on: Option<syn::Path>) -> Self {
329        self.attrs.invalidate_on = invalidate_on;
330        self
331    }
332
333    pub fn cache_if(mut self, cache_if: Option<syn::Path>) -> Self {
334        self.attrs.cache_if = cache_if;
335        self
336    }
337
338    pub fn frequency_weight(mut self, frequency_weight: TokenStream2) -> Self {
339        self.attrs.frequency_weight = frequency_weight;
340        self
341    }
342
343    pub fn window_ratio(mut self, window_ratio: TokenStream2) -> Self {
344        self.attrs.window_ratio = window_ratio;
345        self
346    }
347
348    pub fn sketch_width(mut self, sketch_width: TokenStream2) -> Self {
349        self.attrs.sketch_width = sketch_width;
350        self
351    }
352
353    pub fn sketch_depth(mut self, sketch_depth: TokenStream2) -> Self {
354        self.attrs.sketch_depth = sketch_depth;
355        self
356    }
357
358    pub fn decay_interval(mut self, decay_interval: TokenStream2) -> Self {
359        self.attrs.decay_interval = decay_interval;
360        self
361    }
362
363    /// Apply common attributes from CommonAttributes struct
364    pub fn with_common(mut self, common: CommonAttributes) -> Self {
365        self.attrs.custom_name = common.custom_name;
366        self.attrs.max_memory = common.max_memory;
367        self.attrs.tags = common.tags;
368        self.attrs.events = common.events;
369        self.attrs.dependencies = common.dependencies;
370        self.attrs.invalidate_on = common.invalidate_on;
371        self.attrs.cache_if = common.cache_if;
372        self.attrs.frequency_weight = common.frequency_weight;
373        self.attrs.window_ratio = common.window_ratio;
374        self.attrs.sketch_width = common.sketch_width;
375        self.attrs.sketch_depth = common.sketch_depth;
376        self.attrs.decay_interval = common.decay_interval;
377        self
378    }
379
380    pub fn build(self) -> SyncCacheAttributes {
381        self.attrs
382    }
383}
384
385impl Default for SyncCacheAttributesBuilder {
386    fn default() -> Self {
387        Self::new()
388    }
389}
390
391/// Parse the `limit` attribute
392pub fn parse_limit_attribute(nv: &MetaNameValue) -> TokenStream2 {
393    match &nv.value {
394        Expr::Lit(expr_lit) => match &expr_lit.lit {
395            syn::Lit::Int(lit_int) => match lit_int.base10_parse::<usize>() {
396                Ok(val) => quote! { Some(#val) },
397                Err(_) => quote! { compile_error!("limit must be a valid positive integer") },
398            },
399            _ => quote! { compile_error!("Invalid literal for `limit`: expected integer") },
400        },
401        _ => quote! { compile_error!("Invalid syntax for `limit`: expected `limit = <integer>`") },
402    }
403}
404
405/// Parse the `policy` attribute and return the string value
406pub fn parse_policy_attribute(nv: &MetaNameValue) -> Result<String, TokenStream2> {
407    match &nv.value {
408        Expr::Lit(expr_lit) => match &expr_lit.lit {
409            syn::Lit::Str(s) => {
410                let val = s.value();
411                // Validate the policy value
412                if POLICIES.contains(&val.as_str()) {
413                    Ok(val)
414                } else {
415                    let policies = policies_str_with_separator(", ");
416                    let err_msg = format!("Invalid policy: expected one of {}", policies);
417                    Err(quote! { compile_error!(#err_msg) })
418                }
419            }
420            _ => Err(quote! { compile_error!("Invalid literal for `policy`: expected string") }),
421        },
422        _ => {
423            let policies = policies_str_with_separator("|");
424            let err_msg = format!(
425                "Invalid syntax for `policy`: expected `policy = \"{}\"`",
426                policies
427            );
428            Err(quote! {
429                compile_error!(#err_msg)
430            })
431        }
432    }
433}
434
435/// Parse the `ttl` attribute
436pub fn parse_ttl_attribute(nv: &MetaNameValue) -> TokenStream2 {
437    match &nv.value {
438        Expr::Lit(expr_lit) => match &expr_lit.lit {
439            syn::Lit::Int(lit_int) => {
440                let val = lit_int
441                    .base10_parse::<u64>()
442                    .expect("ttl must be a positive integer (seconds)");
443                quote! { Some(#val) }
444            }
445            _ => quote! { compile_error!("Invalid literal for `ttl`: expected integer (seconds)") },
446        },
447        _ => quote! { compile_error!("Invalid syntax for `ttl`: expected `ttl = <integer>`") },
448    }
449}
450
451/// Parse the `frequency_weight` attribute
452///
453/// Accepts a float value > 0.0. Values of 0.0 or negative are rejected because:
454/// - frequency_weight = 0.0 would cause `0^0` undefined behavior when frequency is 0
455/// - A weight of 0 has no semantic meaning (would make all frequencies equal to 1)
456///
457/// The value is parsed for all policies, but is only used when the TLRU policy is selected.
458///
459/// # Examples
460/// - `frequency_weight = 0.3` → Valid (low weight, emphasizes recency)
461/// - `frequency_weight = 1.0` → Valid (balanced, default behavior)
462/// - `frequency_weight = 1.5` → Valid (high weight, emphasizes frequency)
463/// - `frequency_weight = 0.0` → **Compile error**
464pub fn parse_frequency_weight_attribute(nv: &MetaNameValue) -> TokenStream2 {
465    match &nv.value {
466        Expr::Lit(expr_lit) => match &expr_lit.lit {
467            syn::Lit::Float(lit_float) => {
468                let val = lit_float
469                    .base10_parse::<f64>()
470                    .expect("frequency_weight must be a float");
471                if val <= 0.0 {
472                    quote! { compile_error!("frequency_weight must be > 0.0 (zero would cause 0^0 undefined behavior and has no semantic meaning)") }
473                } else {
474                    quote! { Some(#val) }
475                }
476            }
477            syn::Lit::Int(lit_int) => {
478                // Allow integer literals
479                let val = lit_int
480                    .base10_parse::<u64>()
481                    .expect("frequency_weight must be a number");
482                let val_f64 = val as f64;
483                quote! { Some(#val_f64) }
484            }
485            _ => {
486                quote! { compile_error!("Invalid literal for `frequency_weight`: expected float") }
487            }
488        },
489        _ => {
490            quote! { compile_error!("Invalid syntax for `frequency_weight`: expected `frequency_weight = <float>`") }
491        }
492    }
493}
494
495/// Parse the `name` attribute
496pub fn parse_name_attribute(nv: &MetaNameValue) -> Option<String> {
497    match &nv.value {
498        Expr::Lit(expr_lit) => match &expr_lit.lit {
499            syn::Lit::Str(s) => Some(s.value()),
500            _ => None,
501        },
502        _ => None,
503    }
504}
505
506/// Parse the `max_memory` attribute
507/// Supports formats like: "100MB", "1GB", "500KB", or raw numbers
508pub fn parse_max_memory_attribute(nv: &MetaNameValue) -> TokenStream2 {
509    match &nv.value {
510        Expr::Lit(expr_lit) => match &expr_lit.lit {
511            syn::Lit::Str(s) => {
512                let val_str = s.value();
513                let val_str = val_str.to_uppercase();
514
515                // Parse memory size with units
516                let bytes = if val_str.ends_with("GB") {
517                    let num_str = val_str.trim_end_matches("GB");
518                    match num_str.parse::<usize>() {
519                        Ok(n) => n * 1024 * 1024 * 1024,
520                        Err(_) => {
521                            return quote! { compile_error!("Invalid number format for max_memory") }
522                        }
523                    }
524                } else if val_str.ends_with("MB") {
525                    let num_str = val_str.trim_end_matches("MB");
526                    match num_str.parse::<usize>() {
527                        Ok(n) => n * 1024 * 1024,
528                        Err(_) => {
529                            return quote! { compile_error!("Invalid number format for max_memory") }
530                        }
531                    }
532                } else if val_str.ends_with("KB") {
533                    let num_str = val_str.trim_end_matches("KB");
534                    match num_str.parse::<usize>() {
535                        Ok(n) => n * 1024,
536                        Err(_) => {
537                            return quote! { compile_error!("Invalid number format for max_memory") }
538                        }
539                    }
540                } else {
541                    // Try to parse as raw number (bytes)
542                    match val_str.parse::<usize>() {
543                        Ok(n) => n,
544                        Err(_) => {
545                            return quote! { compile_error!("Invalid format for max_memory: expected \"100MB\", \"1GB\", \"500KB\", or number") }
546                        }
547                    }
548                };
549
550                quote! { Some(#bytes) }
551            }
552            syn::Lit::Int(lit_int) => {
553                let val = lit_int
554                    .base10_parse::<usize>()
555                    .expect("max_memory must be a positive integer (bytes)");
556                quote! { Some(#val) }
557            }
558            _ => {
559                quote! { compile_error!("Invalid literal for `max_memory`: expected string (\"100MB\") or integer") }
560            }
561        },
562        _ => {
563            quote! { compile_error!("Invalid syntax for `max_memory`: expected `max_memory = \"100MB\"`") }
564        }
565    }
566}
567
568/// Parse the `scope` attribute and return the string value
569pub fn parse_scope_attribute(nv: &MetaNameValue) -> Result<String, TokenStream2> {
570    match &nv.value {
571        Expr::Lit(expr_lit) => match &expr_lit.lit {
572            syn::Lit::Str(s) => {
573                let val = s.value();
574                // Validate the scope value
575                if val == "global" || val == "thread" {
576                    Ok(val)
577                } else {
578                    Err(
579                        quote! { compile_error!("Invalid scope: expected \"global\" or \"thread\"") },
580                    )
581                }
582            }
583            _ => Err(quote! { compile_error!("Invalid literal for `scope`: expected string") }),
584        },
585        _ => Err(
586            quote! { compile_error!("Invalid syntax for `scope`: expected `scope = \"global\"|\"thread\"`") },
587        ),
588    }
589}
590
591/// Generate cache key expression based on function arguments (for async macros using format!)
592pub fn generate_key_expr(has_self: bool, arg_pats: &[TokenStream2]) -> TokenStream2 {
593    if has_self {
594        if arg_pats.is_empty() {
595            quote! {{
596                format!("{:?}", self)
597            }}
598        } else {
599            quote! {{
600                let mut __key_parts = Vec::new();
601                __key_parts.push(format!("{:?}", self));
602                #(
603                    __key_parts.push(format!("{:?}", #arg_pats));
604                )*
605                __key_parts.join("|")
606            }}
607        }
608    } else if arg_pats.is_empty() {
609        quote! {{ String::new() }}
610    } else {
611        quote! {{
612            let mut __key_parts = Vec::new();
613            #(
614                __key_parts.push(format!("{:?}", #arg_pats));
615            )*
616            __key_parts.join("|")
617        }}
618    }
619}
620
621/// Generate cache key expression using CacheableKey trait (for sync macros)
622pub fn generate_key_expr_with_cacheable_key(
623    has_self: bool,
624    arg_pats: &[TokenStream2],
625) -> TokenStream2 {
626    if has_self {
627        if arg_pats.is_empty() {
628            quote! {{
629                use cachelito_core::CacheableKey;
630                self.to_cache_key()
631            }}
632        } else {
633            quote! {{
634                use cachelito_core::CacheableKey;
635                let mut __key_parts = Vec::new();
636                __key_parts.push(self.to_cache_key());
637                #(
638                    __key_parts.push((#arg_pats).to_cache_key());
639                )*
640                __key_parts.join("|")
641            }}
642        }
643    } else if arg_pats.is_empty() {
644        quote! {{ String::new() }}
645    } else {
646        quote! {{
647            use cachelito_core::CacheableKey;
648            let mut __key_parts = Vec::new();
649            #(
650                __key_parts.push((#arg_pats).to_cache_key());
651            )*
652            __key_parts.join("|")
653        }}
654    }
655}
656
657/// Parse array of strings from attribute (for tags, events, dependencies)
658///
659/// Supports formats like:
660/// - `tags = ["tag1", "tag2"]`
661/// - `tags = ["single"]`
662/// - `events = ["event1"]`
663pub fn parse_string_array_attribute(nv: &MetaNameValue) -> Result<Vec<String>, TokenStream2> {
664    match &nv.value {
665        Expr::Array(array) => {
666            let mut strings = Vec::new();
667            for elem in &array.elems {
668                match elem {
669                    Expr::Lit(expr_lit) => match &expr_lit.lit {
670                        syn::Lit::Str(s) => {
671                            strings.push(s.value());
672                        }
673                        _ => {
674                            return Err(
675                                quote! { compile_error!("Array elements must be string literals") },
676                            );
677                        }
678                    },
679                    _ => {
680                        return Err(
681                            quote! { compile_error!("Array elements must be string literals") },
682                        );
683                    }
684                }
685            }
686            Ok(strings)
687        }
688        _ => Err(quote! { compile_error!("Expected array of strings like [\"tag1\", \"tag2\"]") }),
689    }
690}
691
692/// Parse the `invalidate_on` attribute
693/// Expects a function path like `invalidate_on = is_stale` or `invalidate_on = my_module::is_stale`
694pub fn parse_invalidate_on_attribute(nv: &MetaNameValue) -> Result<syn::Path, TokenStream2> {
695    match &nv.value {
696        Expr::Path(expr_path) => Ok(expr_path.path.clone()),
697        _ => Err(
698            quote! { compile_error!("Invalid syntax for `invalidate_on`: expected `invalidate_on = function_name`") },
699        ),
700    }
701}
702
703/// Parse the `cache_if` attribute
704/// Expects a function path like `cache_if = should_cache` or `cache_if = my_module::should_cache`
705pub fn parse_cache_if_attribute(nv: &MetaNameValue) -> Result<syn::Path, TokenStream2> {
706    match &nv.value {
707        Expr::Path(expr_path) => Ok(expr_path.path.clone()),
708        _ => Err(
709            quote! { compile_error!("Invalid syntax for `cache_if`: expected `cache_if = function_name`") },
710        ),
711    }
712}
713
714/// Parse the `window_ratio` attribute
715/// Expects a float value between 0.0 and 1.0
716pub fn parse_window_ratio_attribute(nv: &MetaNameValue) -> TokenStream2 {
717    match &nv.value {
718        Expr::Lit(expr_lit) => match &expr_lit.lit {
719            syn::Lit::Float(lit_float) => {
720                let val = lit_float
721                    .base10_parse::<f64>()
722                    .expect("window_ratio must be a float");
723                if val <= 0.0 || val >= 1.0 {
724                    quote! { compile_error!("window_ratio must be between 0.0 and 1.0 (exclusive)") }
725                } else {
726                    quote! { Some(#val) }
727                }
728            }
729            syn::Lit::Int(lit_int) => {
730                // Allow integer literals like 0 (though not semantically valid)
731                let val = lit_int
732                    .base10_parse::<u64>()
733                    .expect("window_ratio must be a number");
734                if val == 0 {
735                    quote! { compile_error!("window_ratio must be between 0.0 and 1.0 (exclusive)") }
736                } else {
737                    let val_f64 = val as f64;
738                    quote! { Some(#val_f64) }
739                }
740            }
741            _ => {
742                quote! { compile_error!("Invalid literal for `window_ratio`: expected float between 0.0 and 1.0") }
743            }
744        },
745        _ => {
746            quote! { compile_error!("Invalid syntax for `window_ratio`: expected `window_ratio = <float>`") }
747        }
748    }
749}
750
751/// Parse the `sketch_width` attribute
752pub fn parse_sketch_width_attribute(nv: &MetaNameValue) -> TokenStream2 {
753    match &nv.value {
754        Expr::Lit(expr_lit) => match &expr_lit.lit {
755            syn::Lit::Int(lit_int) => {
756                let val = lit_int
757                    .base10_parse::<usize>()
758                    .expect("sketch_width must be a positive integer");
759                if val == 0 {
760                    quote! { compile_error!("sketch_width must be greater than 0") }
761                } else {
762                    quote! { Some(#val) }
763                }
764            }
765            _ => quote! { compile_error!("Invalid literal for `sketch_width`: expected integer") },
766        },
767        _ => {
768            quote! { compile_error!("Invalid syntax for `sketch_width`: expected `sketch_width = <integer>`") }
769        }
770    }
771}
772
773/// Parse the `sketch_depth` attribute
774pub fn parse_sketch_depth_attribute(nv: &MetaNameValue) -> TokenStream2 {
775    match &nv.value {
776        Expr::Lit(expr_lit) => match &expr_lit.lit {
777            syn::Lit::Int(lit_int) => {
778                let val = lit_int
779                    .base10_parse::<usize>()
780                    .expect("sketch_depth must be a positive integer");
781                if val == 0 {
782                    quote! { compile_error!("sketch_depth must be greater than 0") }
783                } else {
784                    quote! { Some(#val) }
785                }
786            }
787            _ => quote! { compile_error!("Invalid literal for `sketch_depth`: expected integer") },
788        },
789        _ => {
790            quote! { compile_error!("Invalid syntax for `sketch_depth`: expected `sketch_depth = <integer>`") }
791        }
792    }
793}
794
795/// Parse the `decay_interval` attribute
796pub fn parse_decay_interval_attribute(nv: &MetaNameValue) -> TokenStream2 {
797    match &nv.value {
798        Expr::Lit(expr_lit) => match &expr_lit.lit {
799            syn::Lit::Int(lit_int) => {
800                let val = lit_int
801                    .base10_parse::<u64>()
802                    .expect("decay_interval must be a positive integer (number of operations)");
803                if val == 0 {
804                    quote! { compile_error!("decay_interval must be greater than 0") }
805                } else {
806                    quote! { Some(#val) }
807                }
808            }
809            _ => {
810                quote! { compile_error!("Invalid literal for `decay_interval`: expected integer") }
811            }
812        },
813        _ => {
814            quote! { compile_error!("Invalid syntax for `decay_interval`: expected `decay_interval = <integer>`") }
815        }
816    }
817}
818
819/// Parse common attributes shared between async and sync caches
820/// Returns true if the attribute was recognized and processed
821fn parse_common_attribute(
822    nv: &MetaNameValue,
823    common: &mut CommonAttributes,
824) -> Result<bool, TokenStream2> {
825    if nv.path.is_ident("name") {
826        common.custom_name = parse_name_attribute(nv);
827        Ok(true)
828    } else if nv.path.is_ident("max_memory") {
829        common.max_memory = parse_max_memory_attribute(nv);
830        Ok(true)
831    } else if nv.path.is_ident("tags") {
832        common.tags = parse_string_array_attribute(nv)?;
833        Ok(true)
834    } else if nv.path.is_ident("events") {
835        common.events = parse_string_array_attribute(nv)?;
836        Ok(true)
837    } else if nv.path.is_ident("dependencies") {
838        common.dependencies = parse_string_array_attribute(nv)?;
839        Ok(true)
840    } else if nv.path.is_ident("invalidate_on") {
841        common.invalidate_on = Some(parse_invalidate_on_attribute(nv)?);
842        Ok(true)
843    } else if nv.path.is_ident("cache_if") {
844        common.cache_if = Some(parse_cache_if_attribute(nv)?);
845        Ok(true)
846    } else if nv.path.is_ident("frequency_weight") {
847        common.frequency_weight = parse_frequency_weight_attribute(nv);
848        Ok(true)
849    } else if nv.path.is_ident("window_ratio") {
850        common.window_ratio = parse_window_ratio_attribute(nv);
851        Ok(true)
852    } else if nv.path.is_ident("sketch_width") {
853        common.sketch_width = parse_sketch_width_attribute(nv);
854        Ok(true)
855    } else if nv.path.is_ident("sketch_depth") {
856        common.sketch_depth = parse_sketch_depth_attribute(nv);
857        Ok(true)
858    } else if nv.path.is_ident("decay_interval") {
859        common.decay_interval = parse_decay_interval_attribute(nv);
860        Ok(true)
861    } else {
862        Ok(false)
863    }
864}
865
866/// Parse async cache attributes from a token stream
867pub fn parse_async_attributes(attr: TokenStream2) -> Result<AsyncCacheAttributes, TokenStream2> {
868    use syn::parse::Parser;
869
870    let parser = Punctuated::<MetaNameValue, Token![,]>::parse_terminated;
871    let parsed_args = parser.parse2(attr).map_err(|e| {
872        let msg = format!("Failed to parse attributes: {}", e);
873        quote! { compile_error!(#msg) }
874    })?;
875
876    let mut builder = AsyncCacheAttributes::builder();
877    let mut common = CommonAttributes::new();
878
879    for nv in parsed_args {
880        if nv.path.is_ident("limit") {
881            builder = builder.limit(parse_limit_attribute(&nv));
882        } else if nv.path.is_ident("policy") {
883            match parse_policy_attribute(&nv) {
884                Ok(policy_str) => builder = builder.policy(quote! { #policy_str }),
885                Err(err) => return Err(err),
886            }
887        } else if nv.path.is_ident("ttl") {
888            builder = builder.ttl(parse_ttl_attribute(&nv));
889        } else {
890            // Try to parse as common attribute
891            if !parse_common_attribute(&nv, &mut common)? {
892                // Unknown attribute - generate compile error
893                let attr_name = nv
894                    .path
895                    .get_ident()
896                    .map(|i| i.to_string())
897                    .unwrap_or_else(|| "unknown".to_string());
898                let err_msg = format!(
899                    "Unknown attribute: `{}`. Valid attributes are: limit, policy, ttl, name, max_memory, tags, events, dependencies, invalidate_on, cache_if, frequency_weight, window_ratio, sketch_width, sketch_depth, decay_interval",
900                    attr_name
901                );
902                return Err(quote! { compile_error!(#err_msg) });
903            }
904        }
905    }
906
907    // Apply common attributes using the builder
908    Ok(builder.with_common(common).build())
909}
910
911/// Parse sync cache attributes from a token stream
912pub fn parse_sync_attributes(attr: TokenStream2) -> Result<SyncCacheAttributes, TokenStream2> {
913    use syn::parse::Parser;
914
915    let parser = Punctuated::<MetaNameValue, Token![,]>::parse_terminated;
916    let parsed_args = parser.parse2(attr).map_err(|e| {
917        let msg = format!("Failed to parse attributes: {}", e);
918        quote! { compile_error!(#msg) }
919    })?;
920
921    let mut builder = SyncCacheAttributes::builder();
922    let mut common = CommonAttributes::new();
923
924    for nv in parsed_args {
925        if nv.path.is_ident("limit") {
926            builder = builder.limit(parse_limit_attribute(&nv));
927        } else if nv.path.is_ident("policy") {
928            match parse_policy_attribute(&nv) {
929                Ok(policy_str) => {
930                    let policy_token = if policy_str == "fifo" {
931                        quote! { cachelito_core::EvictionPolicy::FIFO }
932                    } else if policy_str == "lru" {
933                        quote! { cachelito_core::EvictionPolicy::LRU }
934                    } else if policy_str == "lfu" {
935                        quote! { cachelito_core::EvictionPolicy::LFU }
936                    } else if policy_str == "arc" {
937                        quote! { cachelito_core::EvictionPolicy::ARC }
938                    } else if policy_str == "random" {
939                        quote! { cachelito_core::EvictionPolicy::Random }
940                    } else if policy_str == "tlru" {
941                        quote! { cachelito_core::EvictionPolicy::TLRU }
942                    } else if policy_str == "w_tinylfu" {
943                        quote! { cachelito_core::EvictionPolicy::WTinyLFU }
944                    } else {
945                        let policies = policies_str_with_separator(", ");
946                        let err_msg = format!("Invalid policy: expected one of {}", policies);
947                        return Err(quote! { compile_error!(#err_msg) });
948                    };
949                    builder = builder.policy(policy_token);
950                }
951                Err(err) => return Err(err),
952            }
953        } else if nv.path.is_ident("ttl") {
954            builder = builder.ttl(parse_ttl_attribute(&nv));
955        } else if nv.path.is_ident("scope") {
956            match parse_scope_attribute(&nv) {
957                Ok(scope_str) => {
958                    let scope_token = if scope_str == "thread" {
959                        quote! { cachelito_core::CacheScope::ThreadLocal }
960                    } else if scope_str == "global" {
961                        quote! { cachelito_core::CacheScope::Global }
962                    } else {
963                        return Err(
964                            quote! { compile_error!("Invalid scope: expected \"global\" or \"thread\"") },
965                        );
966                    };
967                    builder = builder.scope(scope_token);
968                }
969                Err(err) => return Err(err),
970            }
971        } else {
972            // Try to parse as common attribute
973            if !parse_common_attribute(&nv, &mut common)? {
974                // Unknown attribute - generate compile error
975                let attr_name = nv
976                    .path
977                    .get_ident()
978                    .map(|i| i.to_string())
979                    .unwrap_or_else(|| "unknown".to_string());
980                let err_msg = format!(
981                    "Unknown attribute: `{}`. Valid attributes are: limit, policy, ttl, scope, name, max_memory, tags, events, dependencies, invalidate_on, cache_if, frequency_weight, window_ratio, sketch_width, sketch_depth, decay_interval",
982                    attr_name
983                );
984                return Err(quote! { compile_error!(#err_msg) });
985            }
986        }
987    }
988
989    // Apply common attributes using the builder
990    Ok(builder.with_common(common).build())
991}
992
993#[cfg(test)]
994mod tests {
995    use super::*;
996    use quote::quote;
997    use syn::parse_quote;
998
999    #[test]
1000    fn test_policies_str_with_separator() {
1001        let result = policies_str_with_separator(", ");
1002        assert_eq!(
1003            result,
1004            "\"fifo\", \"lru\", \"lfu\", \"arc\", \"random\", \"tlru\", \"w_tinylfu\""
1005        );
1006
1007        let result = policies_str_with_separator("|");
1008        assert_eq!(
1009            result,
1010            "\"fifo\"|\"lru\"|\"lfu\"|\"arc\"|\"random\"|\"tlru\"|\"w_tinylfu\""
1011        );
1012    }
1013
1014    #[test]
1015    fn test_parse_limit_attribute_valid() {
1016        let nv: MetaNameValue = parse_quote! { limit = 100 };
1017        let result = parse_limit_attribute(&nv);
1018        assert_eq!(result.to_string(), "Some (100usize)");
1019    }
1020
1021    #[test]
1022    fn test_parse_policy_attribute_valid() {
1023        let nv: MetaNameValue = parse_quote! { policy = "fifo" };
1024        let result = parse_policy_attribute(&nv);
1025        assert!(result.is_ok());
1026        assert_eq!(result.unwrap(), "fifo");
1027
1028        let nv: MetaNameValue = parse_quote! { policy = "lru" };
1029        let result = parse_policy_attribute(&nv);
1030        assert!(result.is_ok());
1031        assert_eq!(result.unwrap(), "lru");
1032    }
1033
1034    #[test]
1035    fn test_parse_max_memory_attribute() {
1036        // Test MB format
1037        let nv: MetaNameValue = parse_quote! { max_memory = "100MB" };
1038        let result = parse_max_memory_attribute(&nv);
1039        let expected = 100 * 1024 * 1024;
1040        assert_eq!(result.to_string(), format!("Some ({}usize)", expected));
1041
1042        // Test GB format
1043        let nv: MetaNameValue = parse_quote! { max_memory = "1GB" };
1044        let result = parse_max_memory_attribute(&nv);
1045        let expected = 1024 * 1024 * 1024;
1046        assert_eq!(result.to_string(), format!("Some ({}usize)", expected));
1047
1048        // Test KB format
1049        let nv: MetaNameValue = parse_quote! { max_memory = "500KB" };
1050        let result = parse_max_memory_attribute(&nv);
1051        let expected = 500 * 1024;
1052        assert_eq!(result.to_string(), format!("Some ({}usize)", expected));
1053
1054        // Test raw number
1055        let nv: MetaNameValue = parse_quote! { max_memory = 1024 };
1056        let result = parse_max_memory_attribute(&nv);
1057        assert_eq!(result.to_string(), "Some (1024usize)");
1058
1059        // Test raw number as string
1060        let nv: MetaNameValue = parse_quote! { max_memory = "2048" };
1061        let result = parse_max_memory_attribute(&nv);
1062        assert_eq!(result.to_string(), "Some (2048usize)");
1063    }
1064
1065    #[test]
1066    fn test_parse_policy_attribute_invalid() {
1067        let nv: MetaNameValue = parse_quote! { policy = "invalid" };
1068        let result = parse_policy_attribute(&nv);
1069        assert!(result.is_err());
1070    }
1071
1072    #[test]
1073    fn test_parse_ttl_attribute_valid() {
1074        let nv: MetaNameValue = parse_quote! { ttl = 60 };
1075        let result = parse_ttl_attribute(&nv);
1076        assert_eq!(result.to_string(), "Some (60u64)");
1077    }
1078
1079    #[test]
1080    fn test_parse_name_attribute() {
1081        let nv: MetaNameValue = parse_quote! { name = "my_cache" };
1082        let result = parse_name_attribute(&nv);
1083        assert_eq!(result, Some("my_cache".to_string()));
1084    }
1085
1086    #[test]
1087    fn test_parse_scope_attribute_valid() {
1088        let nv: MetaNameValue = parse_quote! { scope = "global" };
1089        let result = parse_scope_attribute(&nv);
1090        assert!(result.is_ok());
1091        assert_eq!(result.unwrap(), "global");
1092
1093        let nv: MetaNameValue = parse_quote! { scope = "thread" };
1094        let result = parse_scope_attribute(&nv);
1095        assert!(result.is_ok());
1096        assert_eq!(result.unwrap(), "thread");
1097    }
1098
1099    #[test]
1100    fn test_parse_scope_attribute_invalid() {
1101        let nv: MetaNameValue = parse_quote! { scope = "invalid" };
1102        let result = parse_scope_attribute(&nv);
1103        assert!(result.is_err());
1104    }
1105
1106    #[test]
1107    fn test_generate_key_expr_no_self_no_args() {
1108        let result = generate_key_expr(false, &[]);
1109        assert_eq!(result.to_string(), "{ String :: new () }");
1110    }
1111
1112    #[test]
1113    fn test_generate_key_expr_with_self_no_args() {
1114        let result = generate_key_expr(true, &[]);
1115        let expected = quote! {{ format!("{:?}", self) }};
1116        assert_eq!(result.to_string(), expected.to_string());
1117    }
1118
1119    #[test]
1120    fn test_generate_key_expr_with_args() {
1121        let args = vec![quote! { arg1 }, quote! { arg2 }];
1122        let result = generate_key_expr(false, &args);
1123        assert!(result.to_string().contains("__key_parts"));
1124    }
1125
1126    #[test]
1127    fn test_parse_async_attributes_defaults() {
1128        let attrs = parse_async_attributes(quote! {}).unwrap();
1129        assert_eq!(attrs.limit.to_string(), "Option :: < usize > :: None");
1130        assert_eq!(attrs.policy.to_string(), "\"fifo\"");
1131        assert_eq!(attrs.ttl.to_string(), "Option :: < u64 > :: None");
1132        assert_eq!(attrs.custom_name, None);
1133    }
1134
1135    #[test]
1136    fn test_parse_async_attributes_complete() {
1137        let attrs = parse_async_attributes(quote! {
1138            limit = 50,
1139            policy = "lru",
1140            ttl = 120,
1141            name = "test_cache"
1142        })
1143        .unwrap();
1144
1145        assert_eq!(attrs.limit.to_string(), "Some (50usize)");
1146        assert_eq!(attrs.policy.to_string(), "\"lru\"");
1147        assert_eq!(attrs.ttl.to_string(), "Some (120u64)");
1148        assert_eq!(attrs.custom_name, Some("test_cache".to_string()));
1149    }
1150
1151    #[test]
1152    fn test_parse_sync_attributes_defaults() {
1153        let attrs = parse_sync_attributes(quote! {}).unwrap();
1154        assert_eq!(attrs.limit.to_string(), "None");
1155        assert_eq!(
1156            attrs.policy.to_string(),
1157            "cachelito_core :: EvictionPolicy :: FIFO"
1158        );
1159        assert_eq!(
1160            attrs.scope.to_string(),
1161            "cachelito_core :: CacheScope :: Global"
1162        );
1163    }
1164
1165    #[test]
1166    fn test_parse_sync_attributes_complete() {
1167        let attrs = parse_sync_attributes(quote! {
1168            limit = 100,
1169            policy = "arc",
1170            ttl = 300,
1171            scope = "thread",
1172            name = "sync_cache"
1173        })
1174        .unwrap();
1175
1176        assert_eq!(attrs.limit.to_string(), "Some (100usize)");
1177        assert_eq!(
1178            attrs.policy.to_string(),
1179            "cachelito_core :: EvictionPolicy :: ARC"
1180        );
1181        assert_eq!(
1182            attrs.scope.to_string(),
1183            "cachelito_core :: CacheScope :: ThreadLocal"
1184        );
1185        assert_eq!(attrs.custom_name, Some("sync_cache".to_string()));
1186    }
1187
1188    #[test]
1189    fn test_parse_common_attribute_name() {
1190        let nv: MetaNameValue = parse_quote! { name = "test_cache" };
1191        let mut common = CommonAttributes::new();
1192        let result = parse_common_attribute(&nv, &mut common);
1193
1194        assert!(result.is_ok());
1195        assert_eq!(result.unwrap(), true);
1196        assert_eq!(common.custom_name, Some("test_cache".to_string()));
1197    }
1198
1199    #[test]
1200    fn test_parse_common_attribute_max_memory() {
1201        let nv: MetaNameValue = parse_quote! { max_memory = "100MB" };
1202        let mut common = CommonAttributes::new();
1203        let result = parse_common_attribute(&nv, &mut common);
1204
1205        assert!(result.is_ok());
1206        assert_eq!(result.unwrap(), true);
1207        let expected = 100 * 1024 * 1024;
1208        assert_eq!(
1209            common.max_memory.to_string(),
1210            format!("Some ({}usize)", expected)
1211        );
1212    }
1213
1214    #[test]
1215    fn test_parse_common_attribute_tags() {
1216        let nv: MetaNameValue = parse_quote! { tags = ["tag1", "tag2"] };
1217        let mut common = CommonAttributes::new();
1218        let result = parse_common_attribute(&nv, &mut common);
1219
1220        assert!(result.is_ok());
1221        assert_eq!(result.unwrap(), true);
1222        assert_eq!(common.tags, vec!["tag1".to_string(), "tag2".to_string()]);
1223    }
1224
1225    #[test]
1226    fn test_parse_common_attribute_events() {
1227        let nv: MetaNameValue = parse_quote! { events = ["event1"] };
1228        let mut common = CommonAttributes::new();
1229        let result = parse_common_attribute(&nv, &mut common);
1230
1231        assert!(result.is_ok());
1232        assert_eq!(result.unwrap(), true);
1233        assert_eq!(common.events, vec!["event1".to_string()]);
1234    }
1235
1236    #[test]
1237    fn test_parse_common_attribute_dependencies() {
1238        let nv: MetaNameValue = parse_quote! { dependencies = ["dep1", "dep2"] };
1239        let mut common = CommonAttributes::new();
1240        let result = parse_common_attribute(&nv, &mut common);
1241
1242        assert!(result.is_ok());
1243        assert_eq!(result.unwrap(), true);
1244        assert_eq!(
1245            common.dependencies,
1246            vec!["dep1".to_string(), "dep2".to_string()]
1247        );
1248    }
1249
1250    #[test]
1251    fn test_parse_common_attribute_unknown() {
1252        let nv: MetaNameValue = parse_quote! { unknown = "value" };
1253        let mut common = CommonAttributes::new();
1254        let result = parse_common_attribute(&nv, &mut common);
1255
1256        assert!(result.is_ok());
1257        assert_eq!(result.unwrap(), false); // Not recognized
1258    }
1259
1260    #[test]
1261    fn test_parse_common_attribute_invalidate_on() {
1262        let nv: MetaNameValue = parse_quote! { invalidate_on = is_stale };
1263        let mut common = CommonAttributes::new();
1264        let result = parse_common_attribute(&nv, &mut common);
1265
1266        assert!(result.is_ok());
1267        assert_eq!(result.unwrap(), true);
1268        assert!(common.invalidate_on.is_some());
1269        assert_eq!(
1270            common
1271                .invalidate_on
1272                .unwrap()
1273                .segments
1274                .first()
1275                .unwrap()
1276                .ident
1277                .to_string(),
1278            "is_stale"
1279        );
1280    }
1281
1282    #[test]
1283    fn test_parse_string_array_attribute() {
1284        let nv: MetaNameValue = parse_quote! { tags = ["tag1", "tag2", "tag3"] };
1285        let result = parse_string_array_attribute(&nv);
1286        assert!(result.is_ok());
1287        assert_eq!(
1288            result.unwrap(),
1289            vec!["tag1".to_string(), "tag2".to_string(), "tag3".to_string()]
1290        );
1291    }
1292
1293    #[test]
1294    fn test_parse_async_attributes_unknown_attribute() {
1295        let result = parse_async_attributes(quote! {
1296            limit = 100,
1297            unknown_attr = "value"
1298        });
1299
1300        assert!(result.is_err());
1301        if let Err(err) = result {
1302            let err_str = err.to_string();
1303            assert!(err_str.contains("Unknown attribute"));
1304            assert!(err_str.contains("unknown_attr"));
1305        }
1306    }
1307
1308    #[test]
1309    fn test_parse_sync_attributes_unknown_attribute() {
1310        let result = parse_sync_attributes(quote! {
1311            limit = 100,
1312            typo_tag = ["tag1"]
1313        });
1314
1315        assert!(result.is_err());
1316        if let Err(err) = result {
1317            let err_str = err.to_string();
1318            assert!(err_str.contains("Unknown attribute"));
1319            assert!(err_str.contains("typo_tag"));
1320        }
1321    }
1322
1323    #[test]
1324    fn test_parse_async_attributes_typo_in_tags() {
1325        // Test common typo: "tag" instead of "tags"
1326        let result = parse_async_attributes(quote! {
1327            tag = ["user_data"]
1328        });
1329
1330        assert!(result.is_err());
1331        if let Err(err) = result {
1332            let err_str = err.to_string();
1333            assert!(err_str.contains("Unknown attribute"));
1334            assert!(err_str.contains("tag"));
1335        }
1336    }
1337
1338    #[test]
1339    fn test_parse_sync_attributes_typo_in_events() {
1340        // Test common typo: "event" instead of "events"
1341        let result = parse_sync_attributes(quote! {
1342            event = ["user_updated"]
1343        });
1344
1345        assert!(result.is_err());
1346        if let Err(err) = result {
1347            let err_str = err.to_string();
1348            assert!(err_str.contains("Unknown attribute"));
1349            assert!(err_str.contains("event"));
1350        }
1351    }
1352
1353    #[test]
1354    fn test_parse_frequency_weight_valid_float() {
1355        // Test valid float values
1356        let nv: MetaNameValue = parse_quote! { frequency_weight = 0.3 };
1357        let result = parse_frequency_weight_attribute(&nv);
1358        assert_eq!(result.to_string(), "Some (0.3f64)");
1359
1360        let nv: MetaNameValue = parse_quote! { frequency_weight = 1.0 };
1361        let result = parse_frequency_weight_attribute(&nv);
1362        // Note: TokenStream may omit .0 for whole numbers
1363        assert!(result.to_string() == "Some (1f64)" || result.to_string() == "Some (1.0f64)");
1364
1365        let nv: MetaNameValue = parse_quote! { frequency_weight = 1.5 };
1366        let result = parse_frequency_weight_attribute(&nv);
1367        assert_eq!(result.to_string(), "Some (1.5f64)");
1368
1369        let nv: MetaNameValue = parse_quote! { frequency_weight = 2.0 };
1370        let result = parse_frequency_weight_attribute(&nv);
1371        assert!(result.to_string() == "Some (2f64)" || result.to_string() == "Some (2.0f64)");
1372    }
1373
1374    #[test]
1375    fn test_parse_frequency_weight_valid_int() {
1376        // Test that integer literals are accepted and converted to float
1377        let nv: MetaNameValue = parse_quote! { frequency_weight = 1 };
1378        let result = parse_frequency_weight_attribute(&nv);
1379        assert_eq!(result.to_string(), "Some (1f64)");
1380
1381        let nv: MetaNameValue = parse_quote! { frequency_weight = 2 };
1382        let result = parse_frequency_weight_attribute(&nv);
1383        assert_eq!(result.to_string(), "Some (2f64)");
1384    }
1385
1386    #[test]
1387    fn test_parse_frequency_weight_rejects_zero() {
1388        // Test that 0.0 is rejected (would cause 0^0 undefined behavior)
1389        let nv: MetaNameValue = parse_quote! { frequency_weight = 0.0 };
1390        let result = parse_frequency_weight_attribute(&nv);
1391        let result_str = result.to_string();
1392        assert!(result_str.contains("compile_error"));
1393        assert!(result_str.contains("must be > 0.0"));
1394        assert!(result_str.contains("0^0"));
1395    }
1396
1397    #[test]
1398    fn test_parse_frequency_weight_rejects_negative() {
1399        // Test that negative values are rejected
1400        let nv: MetaNameValue = parse_quote! { frequency_weight = -0.5 };
1401        let result = parse_frequency_weight_attribute(&nv);
1402        let result_str = result.to_string();
1403        assert!(result_str.contains("compile_error"));
1404        assert!(result_str.contains("must be > 0.0"));
1405    }
1406
1407    #[test]
1408    fn test_parse_frequency_weight_very_small_positive() {
1409        // Test that very small positive values are accepted
1410        let nv: MetaNameValue = parse_quote! { frequency_weight = 0.01 };
1411        let result = parse_frequency_weight_attribute(&nv);
1412        assert_eq!(result.to_string(), "Some (0.01f64)");
1413
1414        let nv: MetaNameValue = parse_quote! { frequency_weight = 0.001 };
1415        let result = parse_frequency_weight_attribute(&nv);
1416        assert_eq!(result.to_string(), "Some (0.001f64)");
1417    }
1418
1419    #[test]
1420    fn test_parse_frequency_weight_large_values() {
1421        // Test that large values are accepted
1422        let nv: MetaNameValue = parse_quote! { frequency_weight = 10.0 };
1423        let result = parse_frequency_weight_attribute(&nv);
1424        assert!(result.to_string() == "Some (10f64)" || result.to_string() == "Some (10.0f64)");
1425
1426        let nv: MetaNameValue = parse_quote! { frequency_weight = 100.0 };
1427        let result = parse_frequency_weight_attribute(&nv);
1428        assert!(result.to_string() == "Some (100f64)" || result.to_string() == "Some (100.0f64)");
1429    }
1430
1431    #[test]
1432    fn test_async_cache_attributes_builder() {
1433        let attrs = AsyncCacheAttributes::builder()
1434            .limit(quote! { Some(100) })
1435            .policy(quote! { "lru" })
1436            .ttl(quote! { Some(60) })
1437            .custom_name(Some("my_cache".to_string()))
1438            .build();
1439
1440        assert_eq!(attrs.limit.to_string(), "Some (100)");
1441        assert_eq!(attrs.policy.to_string(), "\"lru\"");
1442        assert_eq!(attrs.ttl.to_string(), "Some (60)");
1443        assert_eq!(attrs.custom_name, Some("my_cache".to_string()));
1444    }
1445
1446    #[test]
1447    fn test_sync_cache_attributes_builder() {
1448        let attrs = SyncCacheAttributes::builder()
1449            .limit(quote! { Some(200) })
1450            .policy(quote! { cachelito_core::EvictionPolicy::FIFO })
1451            .scope(quote! { cachelito_core::CacheScope::ThreadLocal })
1452            .ttl(quote! { Some(120) })
1453            .build();
1454
1455        assert_eq!(attrs.limit.to_string(), "Some (200)");
1456        assert_eq!(
1457            attrs.policy.to_string(),
1458            "cachelito_core :: EvictionPolicy :: FIFO"
1459        );
1460        assert_eq!(
1461            attrs.scope.to_string(),
1462            "cachelito_core :: CacheScope :: ThreadLocal"
1463        );
1464        assert_eq!(attrs.ttl.to_string(), "Some (120)");
1465    }
1466
1467    #[test]
1468    fn test_builder_with_common_attributes() {
1469        let mut common = CommonAttributes::new();
1470        common.custom_name = Some("test".to_string());
1471        common.tags = vec!["tag1".to_string(), "tag2".to_string()];
1472        common.frequency_weight = quote! { Some(1.5) };
1473
1474        let attrs = AsyncCacheAttributes::builder()
1475            .limit(quote! { Some(50) })
1476            .with_common(common)
1477            .build();
1478
1479        assert_eq!(attrs.custom_name, Some("test".to_string()));
1480        assert_eq!(attrs.tags, vec!["tag1".to_string(), "tag2".to_string()]);
1481        assert_eq!(attrs.frequency_weight.to_string(), "Some (1.5)");
1482        assert_eq!(attrs.limit.to_string(), "Some (50)");
1483    }
1484}