diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9b218..04593df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - 2025-xx-yy +## [Unreleased] - xxxx-yy-zz ### Added @@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [0.7.0] - 2025-09-09 + +### Changed + +- Conditionally remove `Clone`able need or Err type. So in cases you pass `result` to `kash`, when the return is `Result`, your Err type doesn't need to be `Clone`able. + ## [0.6.0] - 2025-09-08 ### Changed diff --git a/Cargo.toml b/Cargo.toml index bade26b..8940b43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["kash_macros"] [package] name = "kash" -version = "0.6.0" +version = "0.7.0" authors = ["Omid Rad ", "James Kominick "] description = "Function and method cache and memoization" repository = "https://github.com/omid/kash" @@ -51,7 +51,7 @@ disk_store = [ [dependencies] async-trait = "0.1" directories = { version = "6.0", optional = true } -kash_macros = { path = "kash_macros", version = "0.6" } +kash_macros = { path = "kash_macros", version = "0.7" } moka = "0.12" # moka = { version = "0.12" , path = "../moka/" } once_cell = "1" diff --git a/Makefile b/Makefile index aacd44d..200a56c 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,18 @@ # run using `cargo run --example=$EXAMPLE` and run standalone. All features are # **enabled** KASH_BASIC_EXAMPLES = basic \ - complex \ - in_impl \ - once \ - tokio + fib \ + complex \ + in_impl \ + once \ + tokio \ + disk \ + custom_error_clone \ + custom_error_noclone # Same as `KASH_BASIC_EXAMPLES`, but these examples require the `docker/redis` # goal KASH_REDIS_EXAMPLES = redis \ - redis-async + redis-async # Cargo command used to run `run`, `build`, `test`... Useful if you keep # multiple cargo versions installed on your machine diff --git a/examples/custom_error.rs b/examples/custom_error_clone.rs similarity index 97% rename from examples/custom_error.rs rename to examples/custom_error_clone.rs index 5dd91a3..aec2050 100644 --- a/examples/custom_error.rs +++ b/examples/custom_error_clone.rs @@ -10,7 +10,7 @@ enum MyError { Err, } -#[kash(result)] +#[kash] fn slow_fn(n: u32) -> Result { if n == 0 { return Err(MyError::Err); diff --git a/examples/custom_error_noclone.rs b/examples/custom_error_noclone.rs new file mode 100644 index 0000000..81f5b92 --- /dev/null +++ b/examples/custom_error_noclone.rs @@ -0,0 +1,53 @@ +#![allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +use kash::kash; +use std::{ + sync::Arc, + thread::sleep, + time::{Duration, Instant}, +}; + +enum MyError { + Err, +} + +#[kash] +fn slow_fn(n: u32) -> Result> { + if n == 0 { + return Err(Arc::new(MyError::Err)); + } + sleep(Duration::new(1, 0)); + slow_fn(n - 1) +} + +#[kash(result)] +fn slow_fn_with_result_flag(n: u32) -> Result { + if n == 0 { + return Err(MyError::Err); + } + sleep(Duration::new(1, 0)); + slow_fn_with_result_flag(n - 1) +} + +pub fn main() { + println!("Initial run..."); + let now = Instant::now(); + let _ = slow_fn(10); + println!("Elapsed: {}\n", now.elapsed().as_secs()); + + println!("Cached run..."); + let now = Instant::now(); + let _ = slow_fn(10); + println!("Elapsed: {}\n", now.elapsed().as_secs()); + + println!("Initial run..."); + let now = Instant::now(); + let _ = slow_fn_with_result_flag(10); + println!("Elapsed: {}\n", now.elapsed().as_secs()); + + println!("Cached run..."); + let now = Instant::now(); + let _ = slow_fn_with_result_flag(10); + println!("Elapsed: {}\n", now.elapsed().as_secs()); + + println!("done!"); +} diff --git a/fib/kitchen_sink.rs b/examples/fib.rs similarity index 98% rename from fib/kitchen_sink.rs rename to examples/fib.rs index 7d99ee5..1151420 100644 --- a/fib/kitchen_sink.rs +++ b/examples/fib.rs @@ -1,3 +1,8 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::unnecessary_wraps +)] use kash::kash; use std::thread::{sleep, spawn}; use std::time::Duration; diff --git a/kash_macros/Cargo.toml b/kash_macros/Cargo.toml index 39aff20..e2f034f 100644 --- a/kash_macros/Cargo.toml +++ b/kash_macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kash_macros" -version = "0.6.0" +version = "0.7.0" authors = [ "Omid Rad ", "csos95 ", diff --git a/kash_macros/src/lib.rs b/kash_macros/src/lib.rs index ce0b2a4..0fdfc2d 100644 --- a/kash_macros/src/lib.rs +++ b/kash_macros/src/lib.rs @@ -14,15 +14,16 @@ use syn::{ItemFn, parse_macro_input}; /// In the attribute list below, `size`, `eviction_policy` are possible just if it's a memory cache. /// /// # Attributes +/// /// - `name`: (optional, string) Specify the name for the generated cache. Defaults to CONSTANT_CASE name of the function /// - `size`: (optional, string) Specify to keep the number of entries in the cache. Default to unbounded. -/// - `eviction_policy`: (optional, string) Specify the eviction policy, valid options are "lfu" (Least Frequently Used) and "lru" (Least Recently Used). Defaults to "lfu" and it's the most suitable policy for most cases. +/// - `eviction_policy`: (optional, string) Specify the eviction policy, valid options are "lfu" (Least Frequently Used) and "lru" (Least Recently Used). Defaults to "lfu". /// - `ttl`: (optional, string) Specify a cache TTL in seconds. Defaults to unlimited amount of time. /// - `key`: (optional, string) Specify a specific key to use. You need to define the following attributes for a custom `key`, e.g., `key(ty = "String", expr = r#"{ format!("{}:{}", arg1, arg2) }"#)`. By default, use all the arguments of the function as the key. /// - `ty`: (string) Specify type of the key. E.g, `ty = "String"` /// - `expr`: (string expr) Specify an expression used to generate a cache key. /// E.g., `expr = r#"{ format!("{}:{}", arg1, arg2) }"#`. -/// - `result`: (optional) If your function returns a `Result`, only cache `Ok` values returned by the function. +/// - `result`: (optional) If your function returns a `Result`, only cache `Ok` values returned by the function. (Read the note below about `Result`) /// - `option`: (optional) If your function returns an `Option`, only cache `Some` values returned by the function. /// - `in_impl`: (optional) Set it if your function is defined in an `impl` block, otherwise not. /// - `redis`: (optional) Store cached values in Redis. @@ -40,6 +41,12 @@ use syn::{ItemFn, parse_macro_input}; /// to give more control over the connection to the `disk` cache, i.e., useful for controlling the rate at which the cache syncs to disk. /// See the docs of `kash::stores::DiskCacheBuilder::connection_config` for more info. /// +/// # Note +/// +/// - If your function returns a `Result`: +/// - In cases you define `result` in `kash`, the `Err` variant can be anything. +/// - But in cases you want to cache the `Err` variant too (by _not_ defining `result` in `kash`), the error type must be `Clone`able. So in this case, you may need to `Arc` your error type. +/// #[proc_macro_attribute] pub fn kash(args: TokenStream, input: TokenStream) -> TokenStream { let args = match MacroArgs::try_from(args) { diff --git a/kash_macros/src/mem/cache_fn.rs b/kash_macros/src/mem/cache_fn.rs index 042cf1b..e2fdbd2 100644 --- a/kash_macros/src/mem/cache_fn.rs +++ b/kash_macros/src/mem/cache_fn.rs @@ -46,34 +46,37 @@ impl ToTokens for CacheFn<'_> { } else { quote! {} }; - let mut function_call = quote! { - #call_prefix #no_cache_fn_ident(#(#maybe_with_self_names),*) + let function_call = quote! { + #call_prefix #no_cache_fn_ident(#(#maybe_with_self_names),*) #may_await }; - if sig.asyncness.is_none() { - function_call = quote! { - || #function_call - } - } + let may_return = if self.args.option { + quote!(?) + } else { + quote!() + }; - let (insert, may_return_early, may_wrap) = match (self.args.result, self.args.option) { - (false, false) => (quote!(.or_insert_with(#function_call)), quote!(), quote!()), - (true, false) => ( - quote!(.or_try_insert_with(#function_call)), - quote!(.map_err(|e| e.deref().clone())?), - quote!(Ok), - ), + let (insert, may_wrap) = match (self.args.result, self.args.option) { + (false, false) => (quote!(.or_insert(#function_call) #may_await), quote!()), + (true, false) => (quote!(.or_insert(#function_call?) #may_await), quote!(Ok)), (false, true) => ( - quote!(.or_optionally_insert_with(#function_call) ), - quote!(?), + quote!(.or_optionally_insert_with(|| #function_call) #may_await), quote!(Some), ), _ => unreachable!("All errors should be handled in the `MacroArgs` validation methods"), }; let do_set_return_block = quote! { - use std::ops::Deref; - #may_wrap (#local_cache.entry_by_ref(&#key_expr) #insert #may_await #may_return_early .into_value() .clone()) + let val = #local_cache.get(&#key_expr) #may_await; + if let Some(val) = val { + #may_wrap (val) + } else { + #may_wrap (#local_cache + .entry_by_ref(&#key_expr) + #insert + #may_return + .into_value()) + } }; let expanded = quote! {