use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{punctuated::Punctuated, Expr, MetaNameValue, Token};
static POLICIES: &[&str] = &["fifo", "lru", "lfu", "arc"];
pub fn policies_str_with_separator(separator: &str) -> String {
POLICIES
.iter()
.map(|p| format!("\"{}\"", p))
.collect::<Vec<_>>()
.join(separator)
}
pub struct AsyncCacheAttributes {
pub limit: TokenStream2,
pub policy: TokenStream2,
pub ttl: TokenStream2,
pub custom_name: Option<String>,
}
impl Default for AsyncCacheAttributes {
fn default() -> Self {
Self {
limit: quote! { Option::<usize>::None },
policy: quote! { "fifo" },
ttl: quote! { Option::<u64>::None },
custom_name: None,
}
}
}
pub struct SyncCacheAttributes {
pub limit: TokenStream2,
pub policy: TokenStream2,
pub ttl: TokenStream2,
pub scope: TokenStream2,
pub custom_name: Option<String>,
}
impl Default for SyncCacheAttributes {
fn default() -> Self {
Self {
limit: quote! { None },
policy: quote! { cachelito_core::EvictionPolicy::FIFO },
ttl: quote! { None },
scope: quote! { cachelito_core::CacheScope::Global },
custom_name: None,
}
}
}
pub fn parse_limit_attribute(nv: &MetaNameValue) -> TokenStream2 {
match &nv.value {
Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Int(lit_int) => {
let val = lit_int
.base10_parse::<usize>()
.expect("limit must be a positive integer");
quote! { Some(#val) }
}
_ => quote! { compile_error!("Invalid literal for `limit`: expected integer") },
},
_ => quote! { compile_error!("Invalid syntax for `limit`: expected `limit = <integer>`") },
}
}
pub fn parse_policy_attribute(nv: &MetaNameValue) -> Result<String, TokenStream2> {
match &nv.value {
Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => {
let val = s.value();
if POLICIES.contains(&val.as_str()) {
Ok(val)
} else {
let policies = policies_str_with_separator(", ");
let err_msg = format!("Invalid policy: expected one of {}", policies);
Err(quote! { compile_error!(#err_msg) })
}
}
_ => Err(quote! { compile_error!("Invalid literal for `policy`: expected string") }),
},
_ => {
let policies = policies_str_with_separator("|");
let err_msg = format!(
"Invalid syntax for `policy`: expected `policy = \"{}\"`",
policies
);
Err(quote! {
compile_error!(#err_msg)
})
}
}
}
pub fn parse_ttl_attribute(nv: &MetaNameValue) -> TokenStream2 {
match &nv.value {
Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Int(lit_int) => {
let val = lit_int
.base10_parse::<u64>()
.expect("ttl must be a positive integer (seconds)");
quote! { Some(#val) }
}
_ => quote! { compile_error!("Invalid literal for `ttl`: expected integer (seconds)") },
},
_ => quote! { compile_error!("Invalid syntax for `ttl`: expected `ttl = <integer>`") },
}
}
pub fn parse_name_attribute(nv: &MetaNameValue) -> Option<String> {
match &nv.value {
Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => Some(s.value()),
_ => None,
},
_ => None,
}
}
pub fn parse_scope_attribute(nv: &MetaNameValue) -> Result<String, TokenStream2> {
match &nv.value {
Expr::Lit(expr_lit) => match &expr_lit.lit {
syn::Lit::Str(s) => {
let val = s.value();
if val == "global" || val == "thread" {
Ok(val)
} else {
Err(
quote! { compile_error!("Invalid scope: expected \"global\" or \"thread\"") },
)
}
}
_ => Err(quote! { compile_error!("Invalid literal for `scope`: expected string") }),
},
_ => Err(
quote! { compile_error!("Invalid syntax for `scope`: expected `scope = \"global\"|\"thread\"`") },
),
}
}
pub fn generate_key_expr(has_self: bool, arg_pats: &[TokenStream2]) -> TokenStream2 {
if has_self {
if arg_pats.is_empty() {
quote! {{
format!("{:?}", self)
}}
} else {
quote! {{
let mut __key_parts = Vec::new();
__key_parts.push(format!("{:?}", self));
#(
__key_parts.push(format!("{:?}", #arg_pats));
)*
__key_parts.join("|")
}}
}
} else if arg_pats.is_empty() {
quote! {{ String::new() }}
} else {
quote! {{
let mut __key_parts = Vec::new();
#(
__key_parts.push(format!("{:?}", #arg_pats));
)*
__key_parts.join("|")
}}
}
}
pub fn generate_key_expr_with_cacheable_key(
has_self: bool,
arg_pats: &[TokenStream2],
) -> TokenStream2 {
if has_self {
if arg_pats.is_empty() {
quote! {{
use cachelito_core::CacheableKey;
self.to_cache_key()
}}
} else {
quote! {{
use cachelito_core::CacheableKey;
let mut __key_parts = Vec::new();
__key_parts.push(self.to_cache_key());
#(
__key_parts.push((#arg_pats).to_cache_key());
)*
__key_parts.join("|")
}}
}
} else if arg_pats.is_empty() {
quote! {{ String::new() }}
} else {
quote! {{
use cachelito_core::CacheableKey;
let mut __key_parts = Vec::new();
#(
__key_parts.push((#arg_pats).to_cache_key());
)*
__key_parts.join("|")
}}
}
}
pub fn parse_async_attributes(attr: TokenStream2) -> Result<AsyncCacheAttributes, TokenStream2> {
use syn::parse::Parser;
let parser = Punctuated::<MetaNameValue, Token![,]>::parse_terminated;
let parsed_args = parser.parse2(attr).map_err(|e| {
let msg = format!("Failed to parse attributes: {}", e);
quote! { compile_error!(#msg) }
})?;
let mut attrs = AsyncCacheAttributes::default();
for nv in parsed_args {
if nv.path.is_ident("limit") {
attrs.limit = parse_limit_attribute(&nv);
} else if nv.path.is_ident("policy") {
match parse_policy_attribute(&nv) {
Ok(policy_str) => attrs.policy = quote! { #policy_str },
Err(err) => return Err(err),
}
} else if nv.path.is_ident("ttl") {
attrs.ttl = parse_ttl_attribute(&nv);
} else if nv.path.is_ident("name") {
attrs.custom_name = parse_name_attribute(&nv);
}
}
Ok(attrs)
}
pub fn parse_sync_attributes(attr: TokenStream2) -> Result<SyncCacheAttributes, TokenStream2> {
use syn::parse::Parser;
let parser = Punctuated::<MetaNameValue, Token![,]>::parse_terminated;
let parsed_args = parser.parse2(attr).map_err(|e| {
let msg = format!("Failed to parse attributes: {}", e);
quote! { compile_error!(#msg) }
})?;
let mut attrs = SyncCacheAttributes::default();
for nv in parsed_args {
if nv.path.is_ident("limit") {
attrs.limit = parse_limit_attribute(&nv);
} else if nv.path.is_ident("policy") {
match parse_policy_attribute(&nv) {
Ok(policy_str) => {
attrs.policy = if policy_str == "fifo" {
quote! { cachelito_core::EvictionPolicy::FIFO }
} else if policy_str == "lru" {
quote! { cachelito_core::EvictionPolicy::LRU }
} else if policy_str == "lfu" {
quote! { cachelito_core::EvictionPolicy::LFU }
} else if policy_str == "arc" {
quote! { cachelito_core::EvictionPolicy::ARC }
} else {
return Err(
quote! { compile_error!("Invalid policy: expected \"fifo\", \"lru\", \"lfu\", or \"arc\"") },
);
};
}
Err(err) => return Err(err),
}
} else if nv.path.is_ident("ttl") {
attrs.ttl = parse_ttl_attribute(&nv);
} else if nv.path.is_ident("scope") {
match parse_scope_attribute(&nv) {
Ok(scope_str) => {
attrs.scope = if scope_str == "thread" {
quote! { cachelito_core::CacheScope::ThreadLocal }
} else if scope_str == "global" {
quote! { cachelito_core::CacheScope::Global }
} else {
return Err(
quote! { compile_error!("Invalid scope: expected \"global\" or \"thread\"") },
);
};
}
Err(err) => return Err(err),
}
} else if nv.path.is_ident("name") {
attrs.custom_name = parse_name_attribute(&nv);
}
}
Ok(attrs)
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
use syn::parse_quote;
#[test]
fn test_policies_str_with_separator() {
let result = policies_str_with_separator(", ");
assert_eq!(result, "\"fifo\", \"lru\", \"lfu\", \"arc\"");
let result = policies_str_with_separator("|");
assert_eq!(result, "\"fifo\"|\"lru\"|\"lfu\"|\"arc\"");
}
#[test]
fn test_parse_limit_attribute_valid() {
let nv: MetaNameValue = parse_quote! { limit = 100 };
let result = parse_limit_attribute(&nv);
assert_eq!(result.to_string(), "Some (100usize)");
}
#[test]
fn test_parse_policy_attribute_valid() {
let nv: MetaNameValue = parse_quote! { policy = "fifo" };
let result = parse_policy_attribute(&nv);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "fifo");
let nv: MetaNameValue = parse_quote! { policy = "lru" };
let result = parse_policy_attribute(&nv);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "lru");
}
#[test]
fn test_parse_policy_attribute_invalid() {
let nv: MetaNameValue = parse_quote! { policy = "invalid" };
let result = parse_policy_attribute(&nv);
assert!(result.is_err());
}
#[test]
fn test_parse_ttl_attribute_valid() {
let nv: MetaNameValue = parse_quote! { ttl = 60 };
let result = parse_ttl_attribute(&nv);
assert_eq!(result.to_string(), "Some (60u64)");
}
#[test]
fn test_parse_name_attribute() {
let nv: MetaNameValue = parse_quote! { name = "my_cache" };
let result = parse_name_attribute(&nv);
assert_eq!(result, Some("my_cache".to_string()));
}
#[test]
fn test_parse_scope_attribute_valid() {
let nv: MetaNameValue = parse_quote! { scope = "global" };
let result = parse_scope_attribute(&nv);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "global");
let nv: MetaNameValue = parse_quote! { scope = "thread" };
let result = parse_scope_attribute(&nv);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "thread");
}
#[test]
fn test_parse_scope_attribute_invalid() {
let nv: MetaNameValue = parse_quote! { scope = "invalid" };
let result = parse_scope_attribute(&nv);
assert!(result.is_err());
}
#[test]
fn test_generate_key_expr_no_self_no_args() {
let result = generate_key_expr(false, &[]);
assert_eq!(result.to_string(), "{ String :: new () }");
}
#[test]
fn test_generate_key_expr_with_self_no_args() {
let result = generate_key_expr(true, &[]);
let expected = quote! {{ format!("{:?}", self) }};
assert_eq!(result.to_string(), expected.to_string());
}
#[test]
fn test_generate_key_expr_with_args() {
let args = vec![quote! { arg1 }, quote! { arg2 }];
let result = generate_key_expr(false, &args);
assert!(result.to_string().contains("__key_parts"));
}
#[test]
fn test_parse_async_attributes_defaults() {
let attrs = parse_async_attributes(quote! {}).unwrap();
assert_eq!(attrs.limit.to_string(), "Option :: < usize > :: None");
assert_eq!(attrs.policy.to_string(), "\"fifo\"");
assert_eq!(attrs.ttl.to_string(), "Option :: < u64 > :: None");
assert_eq!(attrs.custom_name, None);
}
#[test]
fn test_parse_async_attributes_complete() {
let attrs = parse_async_attributes(quote! {
limit = 50,
policy = "lru",
ttl = 120,
name = "test_cache"
})
.unwrap();
assert_eq!(attrs.limit.to_string(), "Some (50usize)");
assert_eq!(attrs.policy.to_string(), "\"lru\"");
assert_eq!(attrs.ttl.to_string(), "Some (120u64)");
assert_eq!(attrs.custom_name, Some("test_cache".to_string()));
}
#[test]
fn test_parse_sync_attributes_defaults() {
let attrs = parse_sync_attributes(quote! {}).unwrap();
assert_eq!(attrs.limit.to_string(), "None");
assert_eq!(
attrs.policy.to_string(),
"cachelito_core :: EvictionPolicy :: FIFO"
);
assert_eq!(
attrs.scope.to_string(),
"cachelito_core :: CacheScope :: Global"
);
}
#[test]
fn test_parse_sync_attributes_complete() {
let attrs = parse_sync_attributes(quote! {
limit = 100,
policy = "arc",
ttl = 300,
scope = "thread",
name = "sync_cache"
})
.unwrap();
assert_eq!(attrs.limit.to_string(), "Some (100usize)");
assert_eq!(
attrs.policy.to_string(),
"cachelito_core :: EvictionPolicy :: ARC"
);
assert_eq!(
attrs.scope.to_string(),
"cachelito_core :: CacheScope :: ThreadLocal"
);
assert_eq!(attrs.custom_name, Some("sync_cache".to_string()));
}
}