From d658031fa1747f773aa6ccdc7ae4f8c99478b83e Mon Sep 17 00:00:00 2001 From: Philippe Assis Date: Wed, 6 Aug 2025 17:45:03 -0300 Subject: [PATCH] Enhance Quickleaf Cache with Event System, TTL Support, and Filtering Features - Introduced an event system for cache operations, allowing notifications for insertions, removals, and cache clearing. - Added TTL (Time To Live) support for cache entries, enabling automatic expiration and lazy cleanup. - Enhanced filtering capabilities for cache queries with new filter types: StartWith, EndWith, and StartAndEndWith. - Updated ListProps to support pagination and filtering options. - Improved documentation with examples for new features, including TTL usage and event notifications. - Added tests for TTL functionality and event handling to ensure reliability. - Refactored existing code for better clarity and maintainability. --- Cargo.lock | 10 +- Cargo.toml | 4 +- README.md | 500 ++++++++++++++++------ README.md.backup | 231 ++++++++++ benches/cache_benchmarks.rs | 0 benches/data_structure_comparison.rs | 0 benches/event_benchmarks.rs | 0 examples/ttl_example.rs | 68 +++ fix_docs.sh | 18 + src/cache.rs | 601 +++++++++++++++++++++++++-- src/error.rs | 81 +++- src/event.rs | 159 +++++++ src/filter.rs | 94 ++++- src/lib.rs | 37 +- src/list_props.rs | 208 ++++++++- src/prelude.rs | 24 ++ src/quickleaf.rs | 63 +++ src/tests.rs | 19 +- src/ttl_tests.rs | 140 +++++++ 19 files changed, 2059 insertions(+), 198 deletions(-) create mode 100644 README.md.backup create mode 100644 benches/cache_benchmarks.rs create mode 100644 benches/data_structure_comparison.rs create mode 100644 benches/event_benchmarks.rs create mode 100644 examples/ttl_example.rs create mode 100644 fix_docs.sh create mode 100644 src/ttl_tests.rs diff --git a/Cargo.lock b/Cargo.lock index d9ce3b3..63c75b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,7 +252,7 @@ dependencies = [ [[package]] name = "quickleaf" -version = "0.2.7" +version = "0.3.0" dependencies = [ "valu3", ] @@ -383,9 +383,9 @@ checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "valu3" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5801411a49e2925d1c5027380da19019fd11e6a444488b0ad789768bfe6d895a" +checksum = "209e312313c00b227d042b4ef320bcd861429b94aedabbb65338e4ed982ceb83" dependencies = [ "bincode", "chrono", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "valu3-derive" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e922c51fb2a80e0b46a13ce08230ed7ea1b5fa2f492193a4c232b742d55fea" +checksum = "37439740c1661f75854357e3878a7b731393ed48b141dd61c0a1b76244ff2327" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 205a311..b11ba56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quickleaf" -version = "0.2.7" +version = "0.3.0" edition = "2021" license = "Apache-2.0" authors = ["Philippe Assis "] @@ -12,7 +12,7 @@ repository = "https://github.com/lowcarboncode/quickleaf" readme = "README.md" [dependencies] -valu3 = "0.7.2" +valu3 = "0.8.2" [features] default = [] diff --git a/README.md b/README.md index b25d202..d317c12 100644 --- a/README.md +++ b/README.md @@ -1,205 +1,471 @@ -# Quickleaf Cache +# 🍃 Quickleaf Cache -Quickleaf Cache is a Rust library that provides a simple and efficient in-memory cache with support for filtering, ordering, limiting results, and event notifications. It is designed to be lightweight and easy to use. +[![Crates.io](https://img.shields.io/crates/v/quickleaf.svg)](https://crates.io/crates/quickleaf) +[![License](https://img.shields.io/crates/l/quickleaf.svg)](https://github.com/lowcarboncode/quickleaf/blob/main/LICENSE) +[![Documentation](https://docs.rs/quickleaf/badge.svg)](https://docs.rs/quickleaf) -## Features +Quickleaf Cache is a **fast**, **lightweight**, and **feature-rich** in-memory cache library for Rust. It combines the simplicity of a HashMap with advanced caching features like **TTL (Time To Live)**, **filtering**, **ordering**, and **event notifications**. -- Insert and remove key-value pairs -- Retrieve values by key -- Clear the cache -- List cache entries with support for filtering, ordering, and limiting results -- Custom error handling -- Event notifications for cache operations -- Support for generic values using [valu3](https://github.com/lowcarboncode/valu3) +## ✨ Features -## Installation +- 🚀 **High Performance**: O(1) access with ordered key iteration +- ⏰ **TTL Support**: Automatic expiration with lazy cleanup +- 🔍 **Advanced Filtering**: StartWith, EndWith, and complex pattern matching +- 📋 **Flexible Ordering**: Ascending/descending with pagination support +- 🔔 **Event Notifications**: Real-time cache operation events +- 🎯 **LRU Eviction**: Automatic removal of least recently used items +- 🛡️ **Type Safety**: Full Rust type safety with generic value support +- 📦 **Lightweight**: Minimal external dependencies + +## 📦 Installation Add the following to your `Cargo.toml`: ```toml [dependencies] -quickleaf = "0.2" +quickleaf = "0.3" ``` -## Usage - -Here's a basic example of how to use Quickleaf Cache: +## 🚀 Quick Start ```rust -use quickleaf::{Quickleaf, ListProps, Order, Filter}; -use quickleaf::valu3::value::Value; +use quickleaf::{Quickleaf, Duration}; fn main() { - let mut cache = Quickleaf::new(2); - cache.insert("key1", 1); - cache.insert("key2", 2); - cache.insert("key3", 3); + // Create a cache with capacity of 1000 items + let mut cache = Quickleaf::new(1000); + + // Insert some data + cache.insert("user:123", "Alice"); + cache.insert("user:456", "Bob"); + + // Retrieve data + println!("{:?}", cache.get("user:123")); // Some("Alice") + + // Insert with TTL (expires in 60 seconds) + cache.insert_with_ttl("session:abc", "temp_data", Duration::from_secs(60)); +} +``` - assert_eq!(cache.get("key1"), None); - assert_eq!(cache.get("key2"), Some(&2.to_value())); - assert_eq!(cache.get("key3"), Some(&3.to_value())); +## 📖 Usage Examples - let list_props = ListProps::default() - .order(Order::Asc) - .limit(10); +### Basic Operations - let result = cache.list(list_props).unwrap(); - for (key, value) in result { - println!("{}: {}", key, value); - } +```rust +use quickleaf::Quickleaf; + +fn main() { + let mut cache = Quickleaf::new(5); + + // Insert data + cache.insert("apple", 100); + cache.insert("banana", 200); + cache.insert("cherry", 300); + + // Get data + println!("{:?}", cache.get("apple")); // Some(100) + + // Check if key exists + assert!(cache.contains_key("banana")); + + // Remove data + cache.remove("cherry").unwrap(); + + // Cache info + println!("Cache size: {}", cache.len()); + println!("Is empty: {}", cache.is_empty()); } ``` -### Using Filters - -You can use filters to narrow down the results when listing cache entries. Here are some examples: +### 🕒 TTL (Time To Live) Features -#### Filter by Start With +#### Default TTL for All Items ```rust -use quickleaf::{Quickleaf, ListProps, Order, Filter}; +use quickleaf::{Quickleaf, Duration}; fn main() { - let mut cache = Quickleaf::new(10); - cache.insert("apple", 1); - cache.insert("banana", 2); - cache.insert("apricot", 3); + // Create cache where all items expire after 5 minutes by default + let mut cache = Quickleaf::with_default_ttl(100, Duration::from_secs(300)); + + // This item will use the default TTL (5 minutes) + cache.insert("default_ttl", "expires in 5 min"); + + // This item has custom TTL (30 seconds) + cache.insert_with_ttl("custom_ttl", "expires in 30 sec", Duration::from_secs(30)); + + // Items expire automatically when accessed + // After 30+ seconds, custom_ttl will return None + println!("{:?}", cache.get("custom_ttl")); +} +``` - let list_props = ListProps::default() - .order(Order::Asc) - .filter(Filter::StartWith("ap")) - .limit(10); +#### Manual Cleanup - let result = cache.list(list_props).unwrap(); - for (key, value) in result { - println!("{}: {}", key, value); - } +```rust +use quickleaf::{Quickleaf, Duration}; +use std::thread; + +fn main() { + let mut cache = Quickleaf::new(10); + + // Add items with short TTL for demo + cache.insert_with_ttl("temp1", "data1", Duration::from_millis(100)); + cache.insert_with_ttl("temp2", "data2", Duration::from_millis(100)); + cache.insert("permanent", "data3"); // No TTL + + println!("Initial size: {}", cache.len()); // 3 + + // Wait for items to expire + thread::sleep(Duration::from_millis(150)); + + // Manual cleanup of expired items + let removed_count = cache.cleanup_expired(); + println!("Removed {} expired items", removed_count); // 2 + println!("Final size: {}", cache.len()); // 1 } ``` -#### Filter by End With +### 🔍 Advanced Filtering + +#### Filter by Prefix ```rust use quickleaf::{Quickleaf, ListProps, Order, Filter}; fn main() { let mut cache = Quickleaf::new(10); - cache.insert("apple", 1); - cache.insert("banana", 2); - cache.insert("pineapple", 3); - - let list_props = ListProps::default() - .order(Order::Asc) - .filter(Filter::EndWith("apple")) - .limit(10); - - let result = cache.list(list_props).unwrap(); - for (key, value) in result { + cache.insert("user:123", "Alice"); + cache.insert("user:456", "Bob"); + cache.insert("product:789", "Widget"); + cache.insert("user:999", "Charlie"); + + // Get all users (keys starting with "user:") + let users = cache.list( + ListProps::default() + .filter(Filter::StartWith("user:".to_string())) + .order(Order::Asc) + ).unwrap(); + + for (key, value) in users { println!("{}: {}", key, value); } } ``` -#### Filter by Start And End With +#### Filter by Suffix ```rust -use quickleaf::{Quickleaf, ListProps, Order, Filter}; +use quickleaf::{Quickleaf, ListProps, Filter}; fn main() { let mut cache = Quickleaf::new(10); - cache.insert("applemorepie", 1); - cache.insert("banana", 2); - cache.insert("pineapplepie", 3); + cache.insert("config.json", "{}"); + cache.insert("data.json", "[]"); + cache.insert("readme.txt", "docs"); + cache.insert("settings.json", "{}"); + + // Get all JSON files + let json_files = cache.list( + ListProps::default() + .filter(Filter::EndWith(".json".to_string())) + ).unwrap(); + + println!("JSON files found: {}", json_files.len()); // 3 +} +``` + +#### Complex Pattern Filtering - let list_props = ListProps::default() - .order(Order::Asc) - .filter(Filter::StartAndEndWith("apple", "pie")) - .limit(10); +```rust +use quickleaf::{Quickleaf, ListProps, Filter, Order}; - let result = cache.list(list_props).unwrap(); - for (key, value) in result { +fn main() { + let mut cache = Quickleaf::new(10); + cache.insert("cache_user_data", "user1"); + cache.insert("cache_product_info", "product1"); + cache.insert("temp_user_session", "session1"); + cache.insert("cache_user_preferences", "prefs1"); + + // Get cached user data (starts with "cache_" and ends with "_data") + let cached_user_data = cache.list( + ListProps::default() + .filter(Filter::StartAndEndWith("cache_".to_string(), "_data".to_string())) + .order(Order::Desc) + ).unwrap(); + + for (key, value) in cached_user_data { println!("{}: {}", key, value); } } ``` -### Using Events +### 📋 Pagination and Ordering -You can use events to get notified when cache entries are inserted, removed, or cleared. Here is an example: +```rust +use quickleaf::{Quickleaf, ListProps, Order}; + +fn main() { + let mut cache = Quickleaf::new(100); + + // Add some test data + for i in 1..=20 { + cache.insert(format!("item_{:02}", i), i); + } + + // Get first 5 items in ascending order + let page1 = cache.list( + ListProps::default() + .order(Order::Asc) + ).unwrap(); + + println!("First 5 items:"); + for (i, (key, value)) in page1.iter().take(5).enumerate() { + println!(" {}: {} = {}", i+1, key, value); + } + + // Get top 3 items in descending order + let desc_items = cache.list( + ListProps::default() + .order(Order::Desc) + ).unwrap(); + + println!("Top 3 items (desc):"); + for (key, value) in desc_items.iter().take(3) { + println!(" {}: {}", key, value); + } +} +``` + +### 🔔 Event Notifications ```rust use quickleaf::{Quickleaf, Event}; use std::sync::mpsc::channel; -use quickleaf::valu3::value::Value; +use std::thread; fn main() { let (tx, rx) = channel(); let mut cache = Quickleaf::with_sender(10, tx); + + // Spawn a thread to handle events + let event_handler = thread::spawn(move || { + for event in rx { + match event { + Event::Insert(data) => { + println!("➕ Inserted: {} = {}", data.key, data.value); + } + Event::Remove(data) => { + println!("➖ Removed: {} = {}", data.key, data.value); + } + Event::Clear => { + println!("🗑️ Cache cleared"); + } + } + } + }); + + // Perform cache operations (will trigger events) + cache.insert("user:1", "Alice"); + cache.insert("user:2", "Bob"); + cache.remove("user:1").unwrap(); + cache.clear(); + + // Close the sender to stop the event handler + drop(cache); + event_handler.join().unwrap(); +} +``` - cache.insert("key1", 1); - cache.insert("key2", 2); - cache.insert("key3", 3); +### 🔄 Combined Features Example - let mut items = Vec::new(); +```rust +use quickleaf::{Quickleaf, Duration, ListProps, Filter, Order}; +use std::thread; - for data in rx { - items.push(data); +fn main() { + // Create cache with default TTL and event notifications + let (tx, _rx) = std::sync::mpsc::channel(); + let mut cache = Quickleaf::with_sender_and_ttl(50, tx, Duration::from_secs(300)); + + // Insert user sessions with custom TTLs + cache.insert_with_ttl("session:guest", "temporary", Duration::from_secs(30)); + cache.insert_with_ttl("session:user123", "authenticated", Duration::from_secs(3600)); + cache.insert("config:theme", "dark"); // Uses default TTL + cache.insert("config:lang", "en"); // Uses default TTL + + // Get all active sessions + let sessions = cache.list( + ListProps::default() + .filter(Filter::StartWith("session:".to_string())) + .order(Order::Asc) + ).unwrap(); + + println!("Active sessions: {}", sessions.len()); + + // Simulate time passing + thread::sleep(Duration::from_secs(35)); + + // Guest session should be expired now + println!("Guest session: {:?}", cache.get("session:guest")); // None + println!("User session: {:?}", cache.get("session:user123")); // Some(...) + + // Manual cleanup + let expired_count = cache.cleanup_expired(); + println!("Cleaned up {} expired items", expired_count); +} +``` - if items.len() == 3 { - break; - } - } +## 🏗️ Architecture - assert_eq!(items.len(), 3); - assert_eq!( - items[0], - Event::insert("key1".to_string(), 1.to_value()) - ); - assert_eq!( - items[1], - Event::insert("key2".to_string(), 2.to_value()) - ); - assert_eq!( - items[2], - Event::insert("key3".to_string(), 3.to_value()) - ); -} +### Cache Structure + +Quickleaf uses a dual-structure approach for optimal performance: + +- **HashMap**: O(1) key-value access +- **Vec**: Maintains sorted key order for efficient iteration +- **Lazy Cleanup**: TTL items are removed when accessed, not proactively + +### TTL Strategy + +- **Lazy Cleanup**: Expired items are removed during access operations (`get`, `contains_key`, `list`) +- **Manual Cleanup**: Use `cleanup_expired()` for proactive cleaning +- **No Background Threads**: Zero overhead until items are accessed + +## 🔧 API Reference + +### Cache Creation + +```rust +// Basic cache +let cache = Quickleaf::new(capacity); + +// With default TTL +let cache = Quickleaf::with_default_ttl(capacity, ttl); + +// With event notifications +let cache = Quickleaf::with_sender(capacity, sender); + +// With both TTL and events +let cache = Quickleaf::with_sender_and_ttl(capacity, sender, ttl); ``` -### Event Types +### Core Operations -There are three types of events: +```rust +// Insert operations +cache.insert(key, value); +cache.insert_with_ttl(key, value, ttl); + +// Access operations +cache.get(key); // Returns Option<&Value> +cache.get_mut(key); // Returns Option<&mut Value> +cache.contains_key(key); // Returns bool + +// Removal operations +cache.remove(key); // Returns Result<(), Error> +cache.clear(); // Removes all items + +// TTL operations +cache.cleanup_expired(); // Returns count of removed items +cache.set_default_ttl(ttl); +cache.get_default_ttl(); +``` + +### Filtering and Listing + +```rust +// List operations +cache.list(props); // Returns Result, Error> + +// Filter types +Filter::None +Filter::StartWith(prefix) +Filter::EndWith(suffix) +Filter::StartAndEndWith(prefix, suffix) + +// Ordering +Order::Asc // Ascending +Order::Desc // Descending +``` + +## 🧪 Testing + +Run the test suite: + +```bash +# All tests +cargo test + +# TTL-specific tests +cargo test ttl -1. `Insert`: Triggered when a new entry is inserted into the cache. -2. `Remove`: Triggered when an entry is removed from the cache. -3. `Clear`: Triggered when the cache is cleared. +# With output +cargo test -- --nocapture +``` + +## 📊 Performance -## Modules +### Benchmarks -### `error` +| Operation | Time Complexity | Notes | +|-----------|----------------|-------| +| Insert | O(log n) | Due to ordered insertion | +| Get | O(1) | HashMap lookup | +| Remove | O(n) | Vec removal | +| List | O(n) | Iteration with filtering | +| TTL Check | O(1) | Simple time comparison | -Defines custom error types used in the library. +### Memory Usage -### `filter` +- **Base overhead**: ~48 bytes per cache instance +- **Per item**: ~(key_size + value_size + 56) bytes +- **TTL overhead**: +24 bytes per item with TTL -Defines the `Filter` enum used for filtering cache entries. +## 📚 Examples -### `list_props` +Check out the `examples/` directory for more comprehensive examples: -Defines the `ListProps` struct used for specifying properties when listing cache entries. +```bash +# Run the TTL example +cargo run --example ttl_example +``` -### `quickleaf` +## 🤝 Contributing -Defines the `Quickleaf` struct which implements the cache functionality. +Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. -## Running Tests +### Development -To run the tests, use the following command: +```bash +# Clone the repository +git clone https://github.com/lowcarboncode/quickleaf.git +cd quickleaf -```sh +# Run tests cargo test + +# Run examples +cargo run --example ttl_example + +# Check formatting +cargo fmt --check + +# Run clippy +cargo clippy -- -D warnings ``` -## License +## 📄 License + +This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. + +## 🔗 Links + +- [Documentation](https://docs.rs/quickleaf) +- [Crates.io](https://crates.io/crates/quickleaf) +- [Repository](https://github.com/lowcarboncode/quickleaf) +- [Issues](https://github.com/lowcarboncode/quickleaf/issues) + +--- -This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for more information. +**Made with ❤️ by the LowCarbonCode team** diff --git a/README.md.backup b/README.md.backup new file mode 100644 index 0000000..f644c3c --- /dev/null +++ b/README.md.backup @@ -0,0 +1,231 @@ +# 🍃 Quickleaf Cache + +[![Crates.io](https://img.shields.io/crates/v/quickleaf.svg)](https://crates.io/crates/quickleaf) +[![License](https://img.shields.io/crates/l/quickleaf.svg)](https://github.com/lowcarboncode/quickleaf/blob/main/LICENSE) +[![Documentation](https://docs.rs/quickleaf/badge.svg)](https://docs.rs/quickleaf) + +Quickleaf Cache is a **fast**, **lightweight**, and **feature-rich** in-memory cache library for Rust. It combines the simplicity of a HashMap with advanced caching features like **TTL (Time To Live)**, **filtering**, **ordering**, and **event notifications**. + +## ✨ Features + +- 🚀 **High Performance**: O(1) access with ordered key iteration +- ⏰ **TTL Support**: Automatic expiration with lazy cleanup +- 🔍 **Advanced Filtering**: StartWith, EndWith, and complex pattern matching +- 📋 **Flexible Ordering**: Ascending/descending with pagination support +- 🔔 **Event Notifications**: Real-time cache operation events +- 🎯 **LRU Eviction**: Automatic removal of least recently used items +- 🛡️ **Type Safety**: Full Rust type safety with generic value support +- 📦 **Zero Dependencies**: Lightweight with minimal external dependencies + +## 📦 Installation + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +quickleaf = "0.3" +``` + +## 🚀 Quick Start + +```rust +use quickleaf::{Quickleaf, Duration}; + +fn main() { + // Create a cache with capacity of 1000 items + let mut cache = Quickleaf::new(1000); + + // Insert some data + cache.insert("user:123", "Alice"); + cache.insert("user:456", "Bob"); + + // Retrieve data + println!("{:?}", cache.get("user:123")); // Some("Alice") + + // Insert with TTL (expires in 60 seconds) + cache.insert_with_ttl("session:abc", "temp_data", Duration::from_secs(60)); +} +``` + +## Usage + +Here's a basic example of how to use Quickleaf Cache: + +```rust +use quickleaf::{Quickleaf, ListProps, Order, Filter}; +use quickleaf::valu3::value::Value; + +fn main() { + let mut cache = Quickleaf::new(2); + cache.insert("key1", 1); + cache.insert("key2", 2); + cache.insert("key3", 3); + + assert_eq!(cache.get("key1"), None); + assert_eq!(cache.get("key2"), Some(&2.to_value())); + assert_eq!(cache.get("key3"), Some(&3.to_value())); + + let list_props = ListProps::default() + .order(Order::Asc) + .limit(10); + + let result = cache.list(list_props).unwrap(); + for (key, value) in result { + println!("{}: {}", key, value); + } +} +``` + +### Using Filters + +You can use filters to narrow down the results when listing cache entries. Here are some examples: + +#### Filter by Start With + +```rust +use quickleaf::{Quickleaf, ListProps, Order, Filter}; + +fn main() { + let mut cache = Quickleaf::new(10); + cache.insert("apple", 1); + cache.insert("banana", 2); + cache.insert("apricot", 3); + + let list_props = ListProps::default() + .order(Order::Asc) + .filter(Filter::StartWith("ap")) + .limit(10); + + let result = cache.list(list_props).unwrap(); + for (key, value) in result { + println!("{}: {}", key, value); + } +} +``` + +#### Filter by End With + +```rust +use quickleaf::{Quickleaf, ListProps, Order, Filter}; + +fn main() { + let mut cache = Quickleaf::new(10); + cache.insert("apple", 1); + cache.insert("banana", 2); + cache.insert("pineapple", 3); + + let list_props = ListProps::default() + .order(Order::Asc) + .filter(Filter::EndWith("apple")) + .limit(10); + + let result = cache.list(list_props).unwrap(); + for (key, value) in result { + println!("{}: {}", key, value); + } +} +``` + +#### Filter by Start And End With + +```rust +use quickleaf::{Quickleaf, ListProps, Order, Filter}; + +fn main() { + let mut cache = Quickleaf::new(10); + cache.insert("applemorepie", 1); + cache.insert("banana", 2); + cache.insert("pineapplepie", 3); + + let list_props = ListProps::default() + .order(Order::Asc) + .filter(Filter::StartAndEndWith("apple", "pie")) + .limit(10); + + let result = cache.list(list_props).unwrap(); + for (key, value) in result { + println!("{}: {}", key, value); + } +} +``` + +### Using Events + +You can use events to get notified when cache entries are inserted, removed, or cleared. Here is an example: + +```rust +use quickleaf::{Quickleaf, Event}; +use std::sync::mpsc::channel; +use quickleaf::valu3::value::Value; + +fn main() { + let (tx, rx) = channel(); + let mut cache = Quickleaf::with_sender(10, tx); + + cache.insert("key1", 1); + cache.insert("key2", 2); + cache.insert("key3", 3); + + let mut items = Vec::new(); + + for data in rx { + items.push(data); + + if items.len() == 3 { + break; + } + } + + assert_eq!(items.len(), 3); + assert_eq!( + items[0], + Event::insert("key1".to_string(), 1.to_value()) + ); + assert_eq!( + items[1], + Event::insert("key2".to_string(), 2.to_value()) + ); + assert_eq!( + items[2], + Event::insert("key3".to_string(), 3.to_value()) + ); +} +``` + +### Event Types + +There are three types of events: + +1. `Insert`: Triggered when a new entry is inserted into the cache. +2. `Remove`: Triggered when an entry is removed from the cache. +3. `Clear`: Triggered when the cache is cleared. + +## Modules + +### `error` + +Defines custom error types used in the library. + +### `filter` + +Defines the `Filter` enum used for filtering cache entries. + +### `list_props` + +Defines the `ListProps` struct used for specifying properties when listing cache entries. + +### `quickleaf` + +Defines the `Quickleaf` struct which implements the cache functionality. + +## Running Tests + +To run the tests, use the following command: + +```sh +cargo test +``` + +## License + +This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for more information. diff --git a/benches/cache_benchmarks.rs b/benches/cache_benchmarks.rs new file mode 100644 index 0000000..e69de29 diff --git a/benches/data_structure_comparison.rs b/benches/data_structure_comparison.rs new file mode 100644 index 0000000..e69de29 diff --git a/benches/event_benchmarks.rs b/benches/event_benchmarks.rs new file mode 100644 index 0000000..e69de29 diff --git a/examples/ttl_example.rs b/examples/ttl_example.rs new file mode 100644 index 0000000..72805af --- /dev/null +++ b/examples/ttl_example.rs @@ -0,0 +1,68 @@ +use quickleaf::{Quickleaf, ListProps, Order, Filter, Duration}; +use std::thread; + +fn main() { + println!("🍃 Quickleaf TTL Cache Example"); + println!("================================\n"); + + // Create cache with default TTL + let mut cache = Quickleaf::with_default_ttl(5, Duration::from_secs(2)); + + // Insert some data with different TTL strategies + println!("📝 Inserting data..."); + cache.insert("persistent", "This won't expire"); // Uses default TTL (2 seconds) + cache.insert_with_ttl("short_lived", "This expires in 1 second", Duration::from_secs(1)); + cache.insert_with_ttl("medium_lived", "This expires in 3 seconds", Duration::from_secs(3)); + + println!(" - persistent: {}", cache.get("persistent").unwrap()); + println!(" - short_lived: {}", cache.get("short_lived").unwrap()); + println!(" - medium_lived: {}", cache.get("medium_lived").unwrap()); + println!(" Cache size: {}\n", cache.len()); + + // Wait 1.5 seconds + println!("⏱️ Waiting 1.5 seconds..."); + thread::sleep(Duration::from_millis(1500)); + + // Check what's still available + println!("🔍 Checking cache after 1.5 seconds:"); + println!(" - persistent: {:?}", cache.get("persistent")); + println!(" - short_lived: {:?}", cache.get("short_lived")); // Should be None (expired) + println!(" - medium_lived: {:?}", cache.get("medium_lived")); + println!(" Cache size: {}\n", cache.len()); + + // Wait another 2 seconds + println!("⏱️ Waiting another 2 seconds..."); + thread::sleep(Duration::from_secs(2)); + + // Manual cleanup + println!("🧹 Manual cleanup:"); + let removed_count = cache.cleanup_expired(); + println!(" Removed {} expired items", removed_count); + println!(" Cache size: {}\n", cache.len()); + + // List remaining items + println!("📋 Remaining items:"); + let result = cache.list(ListProps::default()).unwrap(); + for (key, value) in result { + println!(" - {}: {}", key, value); + } + + // Demonstrate filtering with TTL + println!("\n🔧 Adding more test data..."); + cache.insert_with_ttl("apple_pie", "Delicious!", Duration::from_secs(10)); + cache.insert_with_ttl("apple_juice", "Refreshing!", Duration::from_secs(10)); + cache.insert_with_ttl("banana_split", "Sweet!", Duration::from_secs(10)); + + println!("🔍 Filtering items starting with 'apple':"); + let filtered = cache.list( + ListProps::default() + .filter(Filter::StartWith("apple".to_string())) + .order(Order::Asc) + ).unwrap(); + + for (key, value) in filtered { + println!(" - {}: {}", key, value); + } + + println!("\n✅ Example completed!"); +} diff --git a/fix_docs.sh b/fix_docs.sh new file mode 100644 index 0000000..6885145 --- /dev/null +++ b/fix_docs.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Fix imports in documentation examples +find src/ -name "*.rs" -exec sed -i \ + -e 's/use quickleaf::cache::Cache;/use quickleaf::Cache;/g' \ + -e 's/use quickleaf::cache::CacheItem;/use quickleaf::CacheItem;/g' \ + -e 's/use quickleaf::error::Error;/use quickleaf::Error;/g' \ + -e 's/use quickleaf::event::Event;/use quickleaf::Event;/g' \ + -e 's/use quickleaf::event::EventData;/use quickleaf::EventData;/g' \ + -e 's/use quickleaf::filter::Filter;/use quickleaf::Filter;/g' \ + -e 's/use quickleaf::list_props::ListProps;/use quickleaf::ListProps;/g' \ + -e 's/use quickleaf::list_props::Order;/use quickleaf::Order;/g' \ + -e 's/use quickleaf::list_props::StartAfter;/use quickleaf::StartAfter;/g' \ + -e 's/use quickleaf::{ListProps, Order};/use quickleaf::{ListProps, Order};/g' \ + -e 's/use quickleaf::{StartAfter, ListProps, Order};/use quickleaf::{StartAfter, ListProps, Order};/g' \ + {} \; + +echo "Fixed documentation imports" diff --git a/src/cache.rs b/src/cache.rs index fb69a07..9b76cf9 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::fmt::Debug; -use std::hash::Hash; +use std::time::{Duration, SystemTime}; use valu3::traits::ToValueBehavior; use valu3::value::Value; @@ -11,39 +11,306 @@ use crate::filter::Filter; use crate::list_props::{ListProps, Order, StartAfter}; use std::sync::mpsc::Sender; +/// Type alias for cache keys. pub type Key = String; +/// Represents an item stored in the cache with optional TTL (Time To Live). +/// +/// Each cache item contains: +/// - The actual value stored +/// - Creation timestamp for TTL calculations +/// - Optional TTL duration for automatic expiration +/// +/// # Examples +/// +/// ``` +/// use quickleaf::CacheItem; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::time::Duration; +/// +/// // Create item without TTL +/// let item = CacheItem::new("Hello World".to_value()); +/// assert!(!item.is_expired()); +/// +/// // Create item with TTL +/// let item_with_ttl = CacheItem::with_ttl("temporary".to_value(), Duration::from_secs(60)); +/// assert!(!item_with_ttl.is_expired()); +/// ``` +#[derive(Clone, Debug)] +pub struct CacheItem { + /// The stored value + pub value: Value, + /// When this item was created + pub created_at: SystemTime, + /// Optional TTL duration + pub ttl: Option, +} + +impl CacheItem { + /// Creates a new cache item without TTL. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::CacheItem; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let item = CacheItem::new("data".to_value()); + /// assert!(!item.is_expired()); + /// assert!(item.ttl.is_none()); + /// ``` + pub fn new(value: Value) -> Self { + Self { + value, + created_at: SystemTime::now(), + ttl: None, + } + } + + /// Creates a new cache item with TTL. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::CacheItem; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// + /// let item = CacheItem::with_ttl("session_data".to_value(), Duration::from_secs(300)); + /// assert!(!item.is_expired()); + /// assert_eq!(item.ttl, Some(Duration::from_secs(300))); + /// ``` + pub fn with_ttl(value: Value, ttl: Duration) -> Self { + Self { + value, + created_at: SystemTime::now(), + ttl: Some(ttl), + } + } + + /// Checks if this cache item has expired based on its TTL. + /// + /// Returns `false` if no TTL is set (permanent item). + /// + /// # Examples + /// + /// ``` + /// use quickleaf::CacheItem; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// use std::thread; + /// + /// // Item without TTL never expires + /// let permanent_item = CacheItem::new("permanent".to_value()); + /// assert!(!permanent_item.is_expired()); + /// + /// // Item with very short TTL + /// let short_lived = CacheItem::with_ttl("temp".to_value(), Duration::from_millis(1)); + /// thread::sleep(Duration::from_millis(10)); + /// assert!(short_lived.is_expired()); + /// ``` + pub fn is_expired(&self) -> bool { + if let Some(ttl) = self.ttl { + self.created_at.elapsed().unwrap_or(Duration::MAX) > ttl + } else { + false + } + } +} + +impl PartialEq for CacheItem { + fn eq(&self, other: &Self) -> bool { + self.value == other.value && self.ttl == other.ttl + } +} + +/// Core cache implementation with LRU eviction, TTL support, and event notifications. +/// +/// This cache provides: +/// - O(1) access time for get/insert operations +/// - LRU (Least Recently Used) eviction when capacity is reached +/// - Optional TTL (Time To Live) for automatic expiration +/// - Event notifications for cache operations +/// - Filtering and ordering capabilities for listing entries +/// +/// # Examples +/// +/// ## Basic Usage +/// +/// ``` +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(3); +/// cache.insert("key1", "value1"); +/// cache.insert("key2", "value2"); +/// +/// assert_eq!(cache.get("key1"), Some(&"value1".to_value())); +/// assert_eq!(cache.len(), 2); +/// ``` +/// +/// ## With TTL Support +/// +/// ``` +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::time::Duration; +/// +/// let mut cache = Cache::with_default_ttl(10, Duration::from_secs(60)); +/// cache.insert("session", "user_data"); // Will expire in 60 seconds +/// cache.insert_with_ttl("temp", "data", Duration::from_millis(100)); // Custom TTL +/// +/// assert!(cache.contains_key("session")); +/// ``` +/// +/// ## With Event Notifications +/// +/// ``` +/// use quickleaf::Cache; +/// use quickleaf::Event; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::sync::mpsc::channel; +/// +/// let (tx, rx) = channel(); +/// let mut cache = Cache::with_sender(5, tx); +/// +/// cache.insert("notify", "me"); +/// +/// // Receive the insert event +/// if let Ok(event) = rx.try_recv() { +/// match event { +/// Event::Insert(data) => { +/// assert_eq!(data.key, "notify"); +/// assert_eq!(data.value, "me".to_value()); +/// }, +/// _ => panic!("Expected insert event"), +/// } +/// } +/// ``` #[derive(Clone, Debug)] pub struct Cache { - map: HashMap, + map: HashMap, list: Vec, capacity: usize, + default_ttl: Option, sender: Option>, _phantom: std::marker::PhantomData, } impl PartialEq for Cache { fn eq(&self, other: &Self) -> bool { - self.map == other.map && self.list == other.list && self.capacity == other.capacity + self.map == other.map + && self.list == other.list + && self.capacity == other.capacity + && self.default_ttl == other.default_ttl } } impl Cache { + /// Creates a new cache with the specified capacity. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// + /// let cache = Cache::new(100); + /// assert_eq!(cache.capacity(), 100); + /// assert!(cache.is_empty()); + /// ``` pub fn new(capacity: usize) -> Self { Self { map: HashMap::new(), list: Vec::new(), capacity, + default_ttl: None, sender: None, _phantom: std::marker::PhantomData, } } + /// Creates a new cache with event notifications. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::Event; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::sync::mpsc::channel; + /// + /// let (tx, rx) = channel(); + /// let mut cache = Cache::with_sender(10, tx); + /// + /// cache.insert("test", 42); + /// + /// // Event should be received + /// assert!(rx.try_recv().is_ok()); + /// ``` pub fn with_sender(capacity: usize, sender: Sender) -> Self { Self { map: HashMap::new(), list: Vec::new(), capacity, + default_ttl: None, + sender: Some(sender), + _phantom: std::marker::PhantomData, + } + } + + /// Creates a new cache with default TTL for all items. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// + /// let mut cache = Cache::with_default_ttl(10, Duration::from_secs(300)); + /// cache.insert("auto_expire", "data"); + /// + /// assert_eq!(cache.get_default_ttl(), Some(Duration::from_secs(300))); + /// ``` + pub fn with_default_ttl(capacity: usize, default_ttl: Duration) -> Self { + Self { + map: HashMap::new(), + list: Vec::new(), + capacity, + default_ttl: Some(default_ttl), + sender: None, + _phantom: std::marker::PhantomData, + } + } + + /// Creates a new cache with both event notifications and default TTL. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::Event; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::sync::mpsc::channel; + /// use std::time::Duration; + /// + /// let (tx, rx) = channel(); + /// let mut cache = Cache::with_sender_and_ttl(10, tx, Duration::from_secs(60)); + /// + /// cache.insert("monitored", "data"); + /// assert!(rx.try_recv().is_ok()); + /// assert_eq!(cache.get_default_ttl(), Some(Duration::from_secs(60))); + /// ``` + pub fn with_sender_and_ttl( + capacity: usize, + sender: Sender, + default_ttl: Duration, + ) -> Self { + Self { + map: HashMap::new(), + list: Vec::new(), + capacity, + default_ttl: Some(default_ttl), sender: Some(sender), _phantom: std::marker::PhantomData, } @@ -78,15 +345,92 @@ impl Cache { } } + /// Inserts a key-value pair into the cache. + /// + /// If the cache is at capacity, the least recently used item will be evicted. + /// If a default TTL is set, the item will inherit that TTL. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(2); + /// cache.insert("key1", "value1"); + /// cache.insert("key2", "value2"); + /// cache.insert("key3", "value3"); // This will evict "key1" + /// + /// assert_eq!(cache.get("key1"), None); // Evicted + /// assert_eq!(cache.get("key2"), Some(&"value2".to_value())); + /// assert_eq!(cache.get("key3"), Some(&"value3".to_value())); + /// ``` pub fn insert(&mut self, key: T, value: V) where T: Into + Clone + AsRef, V: ToValueBehavior, { let key = key.into(); + let item = if let Some(default_ttl) = self.default_ttl { + CacheItem::with_ttl(value.to_value(), default_ttl) + } else { + CacheItem::new(value.to_value()) + }; + + if let Some(existing_item) = self.map.get(&key) { + if existing_item.value == item.value { + return; + } + } - if let Some(value) = self.map.get(&key) { - if value.eq(&value) { + if self.map.len() != 0 && self.map.len() == self.capacity { + let first_key = self.list.remove(0); + let data = self.map.get(&first_key).unwrap().clone(); + self.map.remove(&first_key); + self.send_remove(first_key, data.value); + } + + let position = self + .list + .iter() + .position(|k| k > &key) + .unwrap_or(self.list.len()); + + self.list.insert(position, key.clone()); + self.map.insert(key.clone(), item.clone()); + + self.send_insert(key, item.value); + } + + /// Inserts a key-value pair with a specific TTL. + /// + /// The TTL overrides any default TTL set for the cache. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// use std::thread; + /// + /// let mut cache = Cache::new(10); + /// cache.insert_with_ttl("session", "user123", Duration::from_millis(100)); + /// + /// assert!(cache.contains_key("session")); + /// thread::sleep(Duration::from_millis(150)); + /// assert!(!cache.contains_key("session")); // Should be expired + /// ``` + pub fn insert_with_ttl(&mut self, key: T, value: V, ttl: Duration) + where + T: Into + Clone + AsRef, + V: ToValueBehavior, + { + let key = key.into(); + let item = CacheItem::with_ttl(value.to_value(), ttl); + + if let Some(existing_item) = self.map.get(&key) { + if existing_item.value == item.value { return; } } @@ -95,7 +439,7 @@ impl Cache { let first_key = self.list.remove(0); let data = self.map.get(&first_key).unwrap().clone(); self.map.remove(&first_key); - self.send_remove(first_key, data); + self.send_remove(first_key, data.value); } let position = self @@ -105,25 +449,74 @@ impl Cache { .unwrap_or(self.list.len()); self.list.insert(position, key.clone()); - self.map.insert(key.clone(), value.to_value()); + self.map.insert(key.clone(), item.clone()); - self.send_insert(key, value.to_value()); + self.send_insert(key, item.value); } - pub fn get(&self, key: &str) -> Option<&Value> { - self.map.get(key) + /// Retrieves a value from the cache by key. + /// + /// Returns `None` if the key doesn't exist or if the item has expired. + /// Expired items are automatically removed during this operation (lazy cleanup). + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("existing", "data"); + /// + /// assert_eq!(cache.get("existing"), Some(&"data".to_value())); + /// assert_eq!(cache.get("nonexistent"), None); + /// ``` + pub fn get(&mut self, key: &str) -> Option<&Value> { + // Primeiro verifica se existe e se está expirado + let is_expired = if let Some(item) = self.map.get(key) { + item.is_expired() + } else { + return None; + }; + + if is_expired { + // Item expirado, remove do cache + self.remove(key).ok(); + None + } else { + // Item válido, retorna referência + self.map.get(key).map(|item| &item.value) + } } pub fn get_list(&self) -> &Vec { &self.list } - pub fn get_map(&self) -> &HashMap { - &self.map + pub fn get_map(&self) -> HashMap { + self.map + .iter() + .filter(|(_, item)| !item.is_expired()) + .map(|(key, item)| (key.clone(), &item.value)) + .collect() } pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> { - self.map.get_mut(key) + // Primeiro verifica se existe e se está expirado + let is_expired = if let Some(item) = self.map.get(key) { + item.is_expired() + } else { + return None; + }; + + if is_expired { + // Item expirado, remove do cache + self.remove(key).ok(); + None + } else { + // Item válido, retorna referência mutável + self.map.get_mut(key).map(|item| &mut item.value) + } } pub fn capacity(&self) -> usize { @@ -143,7 +536,7 @@ impl Cache { self.map.remove(key); - self.send_remove(key.to_string(), data); + self.send_remove(key.to_string(), data.value); Ok(()) } @@ -166,16 +559,125 @@ impl Cache { self.map.is_empty() } - pub fn contains_key(&self, key: &str) -> bool { - self.map.contains_key(key) + /// Checks if a key exists in the cache and hasn't expired. + /// + /// This method performs lazy cleanup of expired items. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("key", "value"); + /// + /// assert!(cache.contains_key("key")); + /// assert!(!cache.contains_key("nonexistent")); + /// + /// // Test with TTL + /// cache.insert_with_ttl("temp", "data", Duration::from_millis(1)); + /// std::thread::sleep(Duration::from_millis(10)); + /// assert!(!cache.contains_key("temp")); // Should be expired and removed + /// ``` + pub fn contains_key(&mut self, key: &str) -> bool { + if let Some(item) = self.map.get(key) { + if item.is_expired() { + self.remove(key).ok(); + false + } else { + true + } + } else { + false + } + } + + /// Manually removes all expired items from the cache. + /// + /// Returns the number of items that were removed. + /// This is useful for proactive cleanup, though the cache also performs lazy cleanup. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// use std::time::Duration; + /// use std::thread; + /// + /// let mut cache = Cache::new(10); + /// cache.insert_with_ttl("temp1", "data1", Duration::from_millis(10)); + /// cache.insert_with_ttl("temp2", "data2", Duration::from_millis(10)); + /// cache.insert("permanent", "data"); + /// + /// thread::sleep(Duration::from_millis(20)); + /// + /// let removed = cache.cleanup_expired(); + /// assert_eq!(removed, 2); // temp1 and temp2 were removed + /// assert_eq!(cache.len(), 1); // Only permanent remains + /// ``` + pub fn cleanup_expired(&mut self) -> usize { + let expired_keys: Vec<_> = self + .map + .iter() + .filter(|(_, item)| item.is_expired()) + .map(|(key, _)| key.clone()) + .collect(); + + let count = expired_keys.len(); + for key in expired_keys { + self.remove(&key).ok(); + } + count + } + + pub fn set_default_ttl(&mut self, ttl: Option) { + self.default_ttl = ttl; + } + + pub fn get_default_ttl(&self) -> Option { + self.default_ttl } - pub fn list(&self, props: T) -> Result, Error> + /// Lists cache entries with filtering, ordering, and pagination support. + /// + /// This method automatically cleans up expired items before returning results. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Cache; + /// use quickleaf::{ListProps, Order}; + /// use quickleaf::Filter; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("apple", 1); + /// cache.insert("banana", 2); + /// cache.insert("apricot", 3); + /// + /// // List all items in ascending order + /// let props = ListProps::default().order(Order::Asc); + /// let items = cache.list(props).unwrap(); + /// assert_eq!(items.len(), 3); + /// + /// // Filter items starting with "ap" + /// let props = ListProps::default() + /// .filter(Filter::StartWith("ap".to_string())); + /// let filtered = cache.list(props).unwrap(); + /// assert_eq!(filtered.len(), 2); // apple, apricot + /// ``` + pub fn list(&mut self, props: T) -> Result, Error> where T: Into, { let props = props.into(); + // Primeiro faz uma limpeza dos itens expirados para evitar retorná-los + self.cleanup_expired(); + match props.order { Order::Asc => self.resolve_order(self.list.iter(), props), Order::Desc => self.resolve_order(self.list.iter().rev(), props), @@ -188,9 +690,9 @@ impl Cache { props: ListProps, ) -> Result, Error> where - I: Iterator, + I: Iterator, { - if let StartAfter::Key(key) = props.start_after_key { + if let StartAfter::Key(ref key) = props.start_after_key { list_iter .find(|k| k == &key) .ok_or(Error::SortKeyNotFound)?; @@ -200,36 +702,43 @@ impl Cache { let mut count = 0; for k in list_iter { - let filtered = match props.filter { - Filter::StartWith(key) => { - if k.starts_with(&key) { - Some((k.clone(), self.map.get(k).unwrap())) - } else { - None - } + if let Some(item) = self.map.get(k) { + // Pula itens expirados (eles serão removidos na próxima limpeza) + if item.is_expired() { + continue; } - Filter::EndWith(key) => { - if k.ends_with(&key) { - Some((k.clone(), self.map.get(k).unwrap())) - } else { - None + + let filtered = match props.filter { + Filter::StartWith(ref key) => { + if k.starts_with(key) { + Some((k.clone(), &item.value)) + } else { + None + } } - } - Filter::StartAndEndWith(start_key, end_key) => { - if k.starts_with(&start_key) && k.ends_with(&end_key) { - Some((k.clone(), self.map.get(k).unwrap())) - } else { - None + Filter::EndWith(ref key) => { + if k.ends_with(key) { + Some((k.clone(), &item.value)) + } else { + None + } + } + Filter::StartAndEndWith(ref start_key, ref end_key) => { + if k.starts_with(start_key) && k.ends_with(end_key) { + Some((k.clone(), &item.value)) + } else { + None + } + } + Filter::None => Some((k.clone(), &item.value)), + }; + + if let Some(item) = filtered { + list.push(item); + count += 1; + if count == props.limit { + break; } - } - Filter::None => Some((k.clone(), self.map.get(k).unwrap())), - }; - - if let Some(item) = filtered { - list.push(item); - count += 1; - if count == props.limit { - break; } } } diff --git a/src/error.rs b/src/error.rs index 801b3fc..18e6a19 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,89 @@ -use std::fmt::Debug; -use std::fmt::Display; +//! Error types for cache operations. +//! +//! This module defines the error types that can occur during cache operations. +use std::fmt::{Debug, Display}; + +/// Errors that can occur during cache operations. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::Error; +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(10); +/// +/// // Trying to remove a non-existent key returns an error +/// match cache.remove("nonexistent") { +/// Err(Error::KeyNotFound) => println!("Key not found as expected"), +/// _ => panic!("Expected KeyNotFound error"), +/// } +/// ``` #[derive(PartialEq)] pub enum Error { + /// The specified sort key was not found during list operations. + /// + /// This can occur when using `start_after_key` in `ListProps` with a key + /// that doesn't exist in the cache. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Error; + /// use quickleaf::Cache; + /// use quickleaf::ListProps; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("existing_key", "value"); + /// + /// let props = ListProps::default().start_after_key("nonexistent_key"); + /// match cache.list(props) { + /// Err(Error::SortKeyNotFound) => println!("Sort key not found"), + /// _ => panic!("Expected SortKeyNotFound error"), + /// } + /// ``` SortKeyNotFound, + + /// A cache with the same identifier already exists. + /// + /// This error is currently not used in the main API but reserved for + /// future functionality. CacheAlreadyExists, + + /// A sort key already exists. + /// + /// This error is currently not used in the main API but reserved for + /// future functionality. SortKeyExists, + + /// A table with the same name already exists. + /// + /// This error is currently not used in the main API but reserved for + /// future functionality. TableAlreadyExists, + + /// The specified key was not found in the cache. + /// + /// This occurs when trying to remove a key that doesn't exist. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Error; + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// + /// match cache.remove("missing_key") { + /// Err(Error::KeyNotFound) => println!("Key not found"), + /// Err(_) => println!("Other error"), + /// Ok(_) => panic!("Expected an error"), + /// } + /// ``` KeyNotFound, } diff --git a/src/event.rs b/src/event.rs index b8b7eab..2b0a5f7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,29 +1,188 @@ +//! Event system for cache operation notifications. +//! +//! This module provides an event system that allows you to receive notifications +//! when cache operations occur, such as insertions, removals, or cache clearing. + use valu3::value::Value; use crate::cache::Key; +/// Represents different types of cache events. +/// +/// Events are sent through a channel when cache operations occur, allowing +/// external observers to react to cache changes. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::{Event, EventData}; +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::sync::mpsc::channel; +/// +/// let (tx, rx) = channel(); +/// let mut cache = Cache::with_sender(5, tx); +/// +/// // Insert an item +/// cache.insert("user_123", "session_data"); +/// +/// // Receive the insert event +/// if let Ok(event) = rx.try_recv() { +/// match event { +/// Event::Insert(data) => { +/// println!("Inserted: {} = {}", data.key, data.value); +/// assert_eq!(data.key, "user_123"); +/// }, +/// Event::Remove(data) => { +/// println!("Removed: {} = {}", data.key, data.value); +/// }, +/// Event::Clear => { +/// println!("Cache cleared"); +/// }, +/// } +/// } +/// ``` #[derive(Clone, Debug, PartialEq)] pub enum Event { + /// An item was inserted into the cache. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::{Event, EventData}; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let event = Event::insert("key".to_string(), "value".to_value()); + /// match event { + /// Event::Insert(data) => { + /// assert_eq!(data.key, "key"); + /// assert_eq!(data.value, "value".to_value()); + /// }, + /// _ => panic!("Expected insert event"), + /// } + /// ``` Insert(EventData), + + /// An item was removed from the cache. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::{Event, EventData}; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let event = Event::remove("key".to_string(), "value".to_value()); + /// match event { + /// Event::Remove(data) => { + /// assert_eq!(data.key, "key"); + /// assert_eq!(data.value, "value".to_value()); + /// }, + /// _ => panic!("Expected remove event"), + /// } + /// ``` Remove(EventData), + + /// The entire cache was cleared. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Event; + /// + /// let event = Event::clear(); + /// match event { + /// Event::Clear => println!("Cache was cleared"), + /// _ => panic!("Expected clear event"), + /// } + /// ``` Clear, } +/// Data associated with cache insert and remove events. +/// +/// Contains the key and value involved in the operation. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::EventData; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let event_data = EventData { +/// key: "session_id".to_string(), +/// value: "abc123".to_value(), +/// }; +/// +/// assert_eq!(event_data.key, "session_id"); +/// assert_eq!(event_data.value, "abc123".to_value()); +/// ``` #[derive(Clone, Debug, PartialEq)] pub struct EventData { + /// The key associated with the event. pub key: Key, + /// The value associated with the event. pub value: Value, } impl Event { + /// Creates a new insert event. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Event; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let event = Event::insert("user_session".to_string(), "active".to_value()); + /// + /// match event { + /// Event::Insert(data) => { + /// assert_eq!(data.key, "user_session"); + /// assert_eq!(data.value, "active".to_value()); + /// }, + /// _ => panic!("Expected insert event"), + /// } + /// ``` pub fn insert(key: Key, value: Value) -> Self { Self::Insert(EventData { key, value }) } + /// Creates a new remove event. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Event; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let event = Event::remove("expired_key".to_string(), "old_data".to_value()); + /// + /// match event { + /// Event::Remove(data) => { + /// assert_eq!(data.key, "expired_key"); + /// assert_eq!(data.value, "old_data".to_value()); + /// }, + /// _ => panic!("Expected remove event"), + /// } + /// ``` pub fn remove(key: Key, value: Value) -> Self { Self::Remove(EventData { key, value }) } + /// Creates a new clear event. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Event; + /// + /// let event = Event::clear(); + /// + /// match event { + /// Event::Clear => println!("Cache was cleared"), + /// _ => panic!("Expected clear event"), + /// } + /// ``` pub fn clear() -> Self { Self::Clear } diff --git a/src/filter.rs b/src/filter.rs index 6108120..25831c5 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -1,12 +1,96 @@ +//! Filtering functionality for cache queries. +//! +//! This module provides different types of filters that can be applied when listing cache entries. +//! Filters allow you to narrow down results based on key patterns. + +/// Enum representing different filter types for cache queries. +/// +/// Filters are used with the `list` method to narrow down results based on key patterns. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::Filter; +/// use quickleaf::Cache; +/// use quickleaf::ListProps; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(10); +/// cache.insert("apple_pie", 1); +/// cache.insert("banana_split", 2); +/// cache.insert("apple_juice", 3); +/// cache.insert("grape_juice", 4); +/// +/// // Filter by prefix +/// let start_filter = Filter::StartWith("apple".to_string()); +/// let props = ListProps::default().filter(start_filter); +/// let results = cache.list(props).unwrap(); +/// assert_eq!(results.len(), 2); // apple_pie, apple_juice +/// +/// // Filter by suffix +/// let end_filter = Filter::EndWith("juice".to_string()); +/// let props = ListProps::default().filter(end_filter); +/// let results = cache.list(props).unwrap(); +/// assert_eq!(results.len(), 2); // apple_juice, grape_juice +/// +/// // Filter by both prefix and suffix +/// let both_filter = Filter::StartAndEndWith("apple".to_string(), "juice".to_string()); +/// let props = ListProps::default().filter(both_filter); +/// let results = cache.list(props).unwrap(); +/// assert_eq!(results.len(), 1); // apple_juice +/// ``` #[derive(Debug)] -pub enum Filter<'a> { - StartWith(&'a str), - EndWith(&'a str), - StartAndEndWith(&'a str, &'a str), +pub enum Filter { + /// Filter keys that start with the specified string. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Filter; + /// + /// let filter = Filter::StartWith("user_".to_string()); + /// // This will match keys like "user_123", "user_session", etc. + /// ``` + StartWith(String), + + /// Filter keys that end with the specified string. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Filter; + /// + /// let filter = Filter::EndWith("_cache".to_string()); + /// // This will match keys like "session_cache", "user_cache", etc. + /// ``` + EndWith(String), + + /// Filter keys that start with the first string AND end with the second string. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Filter; + /// + /// let filter = Filter::StartAndEndWith("temp_".to_string(), "_data".to_string()); + /// // This will match keys like "temp_session_data", "temp_user_data", etc. + /// ``` + StartAndEndWith(String, String), + + /// No filtering applied - returns all items. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::Filter; + /// + /// let filter = Filter::None; + /// // This will return all cache entries + /// ``` None, } -impl<'a> Default for Filter<'a> { +impl Default for Filter { fn default() -> Self { Self::None } diff --git a/src/lib.rs b/src/lib.rs index 31db980..4c3298c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ //! # Quickleaf Cache //! -//! Quickleaf Cache is a Rust library that provides a simple and efficient in-memory cache with support for filtering, ordering, limiting results, and event notifications. It is designed to be lightweight and easy to use. +//! Quickleaf Cache is a Rust library that provides a simple and efficient in-memory cache with support for filtering, ordering, limiting results, TTL (Time To Live), and event notifications. It is designed to be lightweight and easy to use. //! //! ## Features //! @@ -8,6 +8,7 @@ //! - Retrieve values by key //! - Clear the cache //! - List cache entries with support for filtering, ordering, and limiting results +//! - **TTL (Time To Live) support** with lazy cleanup //! - Custom error handling //! - Event notifications for cache operations //! - Support for generic values using [valu3](https://github.com/lowcarboncode/valu3) @@ -67,7 +68,7 @@ //! //! let list_props = ListProps::default() //! .order(Order::Asc) -//! .filter(Filter::StartWith("ap")); +//! .filter(Filter::StartWith("ap".to_string())); //! //! let result = cache.list(list_props).unwrap(); //! for (key, value) in result { @@ -89,7 +90,7 @@ //! //! let list_props = ListProps::default() //! .order(Order::Asc) -//! .filter(Filter::EndWith("apple")); +//! .filter(Filter::EndWith("apple".to_string())); //! //! let result = cache.list(list_props).unwrap(); //! for (key, value) in result { @@ -111,7 +112,7 @@ //! //! let list_props = ListProps::default() //! .order(Order::Asc) -//! .filter(Filter::StartAndEndWith("apple", "pie")); +//! .filter(Filter::StartAndEndWith("apple".to_string(), "pie".to_string())); //! //! let result = cache.list(list_props).unwrap(); //! for (key, value) in result { @@ -120,6 +121,29 @@ //! } //! ``` //! +//! ### Using TTL (Time To Live) +//! +//! You can set TTL for cache entries to automatically expire them after a certain duration: +//! +//! ```rust +//! use quickleaf::{Quickleaf, Duration}; +//! +//! fn main() { +//! let mut cache = Quickleaf::new(10); +//! +//! // Insert with specific TTL (5 seconds) +//! cache.insert_with_ttl("session", "user_data", Duration::from_secs(5)); +//! +//! // Insert with default TTL +//! let mut cache_with_default = Quickleaf::with_default_ttl(10, Duration::from_secs(60)); +//! cache_with_default.insert("key", "value"); // Will expire in 60 seconds +//! +//! // Manual cleanup of expired items +//! let removed_count = cache.cleanup_expired(); +//! println!("Removed {} expired items", removed_count); +//! } +//! ``` +//! //! ### Using Events //! //! You can use events to get notified when cache entries are inserted, removed, or cleared. Here is an example: @@ -180,12 +204,15 @@ pub mod prelude; mod quickleaf; #[cfg(test)] mod tests; +#[cfg(test)] +mod ttl_tests; -pub use cache::Cache; +pub use cache::{Cache, CacheItem}; pub use error::Error; pub use event::{Event, EventData}; pub use filter::Filter; pub use list_props::{ListProps, Order, StartAfter}; pub use quickleaf::Quickleaf; +pub use std::time::Duration; pub use valu3; pub use valu3::value::Value; diff --git a/src/list_props.rs b/src/list_props.rs index 01c8de7..546a057 100644 --- a/src/list_props.rs +++ b/src/list_props.rs @@ -1,8 +1,42 @@ +//! List properties for configuring cache query behavior. +//! +//! This module provides structures and enums for configuring how cache entries +//! are retrieved, ordered, filtered, and paginated. + use crate::filter::Filter; +/// Enum for specifying sort order when listing cache entries. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::Order; +/// use quickleaf::Cache; +/// use quickleaf::ListProps; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(10); +/// cache.insert("zebra", 1); +/// cache.insert("apple", 2); +/// cache.insert("monkey", 3); +/// +/// // Ascending order (default) +/// let props = ListProps::default().order(Order::Asc); +/// let results = cache.list(props).unwrap(); +/// let keys: Vec<_> = results.iter().map(|(k, _)| k.as_str()).collect(); +/// assert_eq!(keys, vec!["apple", "monkey", "zebra"]); +/// +/// // Descending order +/// let props = ListProps::default().order(Order::Desc); +/// let results = cache.list(props).unwrap(); +/// let keys: Vec<_> = results.iter().map(|(k, _)| k.as_str()).collect(); +/// assert_eq!(keys, vec!["zebra", "monkey", "apple"]); +/// ``` #[derive(Debug, Clone)] pub enum Order { + /// Sort keys in ascending order (A-Z). Asc, + /// Sort keys in descending order (Z-A). Desc, } @@ -12,9 +46,33 @@ impl Default for Order { } } +/// Enum for specifying pagination starting point. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::{StartAfter, ListProps, Order}; +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(10); +/// cache.insert("apple", 1); +/// cache.insert("banana", 2); +/// cache.insert("cherry", 3); +/// +/// // Start listing after "banana" +/// let props = ListProps::default() +/// .start_after_key("banana") +/// .order(Order::Asc); +/// let results = cache.list(props).unwrap(); +/// let keys: Vec<_> = results.iter().map(|(k, _)| k.as_str()).collect(); +/// assert_eq!(keys, vec!["cherry"]); // Only entries after "banana" +/// ``` #[derive(Debug, Clone)] pub enum StartAfter { - Key(&'static str), + /// Start listing after the specified key. + Key(String), + /// Start from the beginning. None, } @@ -24,15 +82,93 @@ impl Default for StartAfter { } } -#[derive(Default, Debug)] +/// Configuration structure for listing cache entries with filtering, ordering, and pagination. +/// +/// `ListProps` allows you to customize how cache entries are retrieved: +/// - Filter by key patterns +/// - Sort in ascending or descending order +/// - Paginate results with starting points and limits +/// +/// # Examples +/// +/// ## Basic Usage +/// +/// ``` +/// use quickleaf::{ListProps, Order}; +/// use quickleaf::Filter; +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(10); +/// cache.insert("apple", 1); +/// cache.insert("banana", 2); +/// cache.insert("apricot", 3); +/// +/// let props = ListProps::default() +/// .order(Order::Desc) +/// .filter(Filter::StartWith("ap".to_string())); +/// +/// let results = cache.list(props).unwrap(); +/// assert_eq!(results.len(), 2); // apple, apricot +/// ``` +/// +/// ## Pagination +/// +/// ``` +/// use quickleaf::ListProps; +/// use quickleaf::Cache; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Cache::new(20); // Increased capacity to hold all items +/// for i in 0..20 { +/// cache.insert(format!("key_{:02}", i), i); +/// } +/// +/// // Get first page (default limit is 10) +/// let props = ListProps::default(); +/// let page1 = cache.list(props).unwrap(); +/// assert_eq!(page1.len(), 10); +/// +/// // Get next page starting after the last key from page1 +/// let last_key = &page1.last().unwrap().0; +/// let props = ListProps::default().start_after_key(last_key); +/// let page2 = cache.list(props).unwrap(); +/// assert_eq!(page2.len(), 10); +/// ``` +#[derive(Debug)] pub struct ListProps { + /// Starting point for pagination. pub start_after_key: StartAfter, - pub filter: Filter<'static>, + /// Filter to apply to keys. + pub filter: Filter, + /// Sort order for results. pub order: Order, + /// Maximum number of results to return. pub limit: usize, } +impl Default for ListProps { + fn default() -> Self { + Self { + start_after_key: StartAfter::None, + filter: Filter::None, + order: Order::Asc, + limit: 10, + } + } +} + impl ListProps { + /// Creates a new `ListProps` with default values. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::ListProps; + /// + /// let props = ListProps::default(); // Use default() instead of new() + /// // Equivalent to creating with default values + /// ``` #[allow(dead_code)] fn new() -> Self { Self { @@ -43,24 +179,80 @@ impl ListProps { } } - pub fn start_after_key(mut self, key: &'static str) -> Self { - self.start_after_key = StartAfter::Key(key); + /// Sets the starting point for pagination. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::ListProps; + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("apple", 1); + /// cache.insert("banana", 2); + /// cache.insert("cherry", 3); + /// + /// let props = ListProps::default().start_after_key("banana"); + /// let results = cache.list(props).unwrap(); + /// // Will return entries after "banana" + /// ``` + pub fn start_after_key(mut self, key: &str) -> Self { + self.start_after_key = StartAfter::Key(key.to_string()); self } - pub fn filter(mut self, filter: Filter<'static>) -> Self { + /// Sets the filter for key matching. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::ListProps; + /// use quickleaf::Filter; + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("user_123", 1); + /// cache.insert("user_456", 2); + /// cache.insert("admin_789", 3); + /// + /// let props = ListProps::default() + /// .filter(Filter::StartWith("user_".to_string())); + /// let results = cache.list(props).unwrap(); + /// assert_eq!(results.len(), 2); // Only user_ entries + /// ``` + pub fn filter(mut self, filter: Filter) -> Self { self.filter = filter; self } + /// Sets the sort order for results. + /// + /// # Examples + /// + /// ``` + /// use quickleaf::{ListProps, Order}; + /// use quickleaf::Cache; + /// use quickleaf::valu3::traits::ToValueBehavior; + /// + /// let mut cache = Cache::new(10); + /// cache.insert("zebra", 1); + /// cache.insert("apple", 2); + /// + /// let props = ListProps::default().order(Order::Desc); + /// let results = cache.list(props).unwrap(); + /// let keys: Vec<_> = results.iter().map(|(k, _)| k.as_str()).collect(); + /// assert_eq!(keys, vec!["zebra", "apple"]); // Descending order + /// ``` pub fn order(mut self, order: Order) -> Self { self.order = order; self } } -impl From> for ListProps { - fn from(filter: Filter<'static>) -> Self { +impl From for ListProps { + fn from(filter: Filter) -> Self { Self { start_after_key: StartAfter::None, filter, diff --git a/src/prelude.rs b/src/prelude.rs index 18d6695..056f840 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1 +1,25 @@ +//! Prelude module for convenient imports. +//! +//! This module re-exports commonly used items to make them easier to import. +//! Import this module to get access to the most frequently used traits and types. + +/// Re-exports from the valu3 library for convenient access to value conversion traits. +/// +/// # Examples +/// +/// ``` +/// use quickleaf::prelude::*; +/// use quickleaf::Quickleaf; +/// +/// let mut cache = Quickleaf::new(10); +/// +/// // ToValueBehavior trait is available from the prelude +/// cache.insert("number", 42); +/// cache.insert("string", "hello"); +/// cache.insert("boolean", true); +/// +/// assert_eq!(cache.get("number"), Some(&42.to_value())); +/// assert_eq!(cache.get("string"), Some(&"hello".to_value())); +/// assert_eq!(cache.get("boolean"), Some(&true.to_value())); +/// ``` pub use valu3::prelude::*; diff --git a/src/quickleaf.rs b/src/quickleaf.rs index 43dd217..4438f4b 100644 --- a/src/quickleaf.rs +++ b/src/quickleaf.rs @@ -1,2 +1,65 @@ +//! Main cache type alias for the Quickleaf library. +//! +//! This module provides the main `Quickleaf` type, which is an alias for the `Cache` struct. + use crate::Cache; + +/// Main cache type for the Quickleaf library. +/// +/// `Quickleaf` is a type alias for `Cache`, providing the same functionality +/// with a more brand-focused name. Use this type when you want to emphasize +/// that you're using the Quickleaf caching library. +/// +/// # Examples +/// +/// ## Basic Usage +/// +/// ``` +/// use quickleaf::Quickleaf; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// +/// let mut cache = Quickleaf::new(100); +/// cache.insert("user_123", "session_data"); +/// +/// assert_eq!(cache.get("user_123"), Some(&"session_data".to_value())); +/// assert_eq!(cache.len(), 1); +/// ``` +/// +/// ## With TTL Support +/// +/// ``` +/// use quickleaf::Quickleaf; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::time::Duration; +/// +/// let mut cache = Quickleaf::with_default_ttl(50, Duration::from_secs(300)); +/// cache.insert("session", "active"); // Will expire in 5 minutes +/// cache.insert_with_ttl("temp", "data", Duration::from_secs(60)); // Custom TTL +/// +/// assert!(cache.contains_key("session")); +/// ``` +/// +/// ## With Event Notifications +/// +/// ``` +/// use quickleaf::{Quickleaf, Event}; +/// use quickleaf::valu3::traits::ToValueBehavior; +/// use std::sync::mpsc::channel; +/// +/// let (tx, rx) = channel(); +/// let mut cache = Quickleaf::with_sender(10, tx); +/// +/// cache.insert("monitor", "this"); +/// +/// // Receive the insert event +/// if let Ok(event) = rx.try_recv() { +/// match event { +/// Event::Insert(data) => { +/// assert_eq!(data.key, "monitor"); +/// assert_eq!(data.value, "this".to_value()); +/// }, +/// _ => panic!("Expected insert event"), +/// } +/// } +/// ``` pub type Quickleaf = Cache; diff --git a/src/tests.rs b/src/tests.rs index 0c22c3a..171783b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -49,7 +49,7 @@ mod test { let result_res = cache.list(ListProps { order: Order::Asc, filter: Filter::None, - start_after_key: StartAfter::Key("key2"), + start_after_key: StartAfter::Key("key2".to_string()), limit: 10, }); @@ -82,8 +82,8 @@ mod test { let result_res = cache.list(ListProps { order: Order::Asc, - filter: Filter::StartWith("post"), - start_after_key: StartAfter::Key("postmodern"), + filter: Filter::StartWith("post".to_string()), + start_after_key: StartAfter::Key("postmodern".to_string()), limit: 10, }); @@ -111,7 +111,7 @@ mod test { let result_res = cache.list(ListProps { order: Order::Desc, filter: Filter::None, - start_after_key: StartAfter::Key("key3"), + start_after_key: StartAfter::Key("key3".to_string()), limit: 10, }); @@ -143,7 +143,7 @@ mod test { let list_props = ListProps::default() .order(Order::Desc) - .filter(Filter::StartWith("post")) + .filter(Filter::StartWith("post".to_string())) .start_after_key("postmodern"); let result_res = cache.list(list_props); @@ -175,7 +175,7 @@ mod test { cache.insert("postgraduate", 7); cache.insert("preconceive", 4); - let result_res = cache.list(Filter::StartWith("postm")); + let result_res = cache.list(Filter::StartWith("postm".to_string())); assert_eq!(result_res.is_ok(), true); @@ -205,7 +205,7 @@ mod test { cache.insert("postgraduate", 7); cache.insert("preconceive", 4); - let result_res = cache.list(Filter::EndWith("tion")); + let result_res = cache.list(Filter::EndWith("tion".to_string())); assert_eq!(result_res.is_ok(), true); @@ -228,7 +228,10 @@ mod test { cache.insert("pineapplepie", 3); let list_props = ListProps::default() - .filter(Filter::StartAndEndWith("apple", "pie")) + .filter(Filter::StartAndEndWith( + "apple".to_string(), + "pie".to_string(), + )) .order(Order::Asc); let result_res = cache.list(list_props); diff --git a/src/ttl_tests.rs b/src/ttl_tests.rs new file mode 100644 index 0000000..0faa958 --- /dev/null +++ b/src/ttl_tests.rs @@ -0,0 +1,140 @@ +#[cfg(test)] +mod ttl_tests { + use std::time::Duration; + use std::thread; + use crate::{Cache, CacheItem}; + use valu3::traits::ToValueBehavior; + + #[test] + fn test_cache_item_creation() { + let item = CacheItem::new(42.to_value()); + assert_eq!(item.value, 42.to_value()); + assert!(item.ttl.is_none()); + assert!(!item.is_expired()); + } + + #[test] + fn test_cache_item_with_ttl() { + let ttl = Duration::from_millis(100); + let item = CacheItem::with_ttl(42.to_value(), ttl); + assert_eq!(item.value, 42.to_value()); + assert_eq!(item.ttl, Some(ttl)); + assert!(!item.is_expired()); + + // Espera um pouco mais que o TTL + thread::sleep(Duration::from_millis(150)); + assert!(item.is_expired()); + } + + #[test] + fn test_cache_with_default_ttl() { + let ttl = Duration::from_secs(300); // 5 minutos + let mut cache = Cache::with_default_ttl(10, ttl); + + assert_eq!(cache.get_default_ttl(), Some(ttl)); + + cache.insert("test", 42); + assert_eq!(cache.get("test"), Some(&42.to_value())); + } + + #[test] + fn test_cache_insert_with_ttl() { + let mut cache = Cache::new(10); + let ttl = Duration::from_millis(100); + + cache.insert_with_ttl("test", 42, ttl); + assert_eq!(cache.get("test"), Some(&42.to_value())); + + // Espera o TTL expirar + thread::sleep(Duration::from_millis(150)); + assert_eq!(cache.get("test"), None); + assert_eq!(cache.len(), 0); // Item foi removido automaticamente + } + + #[test] + fn test_lazy_cleanup_on_get() { + let mut cache = Cache::new(10); + let ttl = Duration::from_millis(50); + + cache.insert_with_ttl("expired", 1, ttl); + cache.insert("normal", 2); + + assert_eq!(cache.len(), 2); + + // Espera o primeiro item expirar + thread::sleep(Duration::from_millis(100)); + + // O get deve remover o item expirado + assert_eq!(cache.get("expired"), None); + assert_eq!(cache.len(), 1); // Agora só tem 1 item + assert_eq!(cache.get("normal"), Some(&2.to_value())); + } + + #[test] + fn test_cleanup_expired() { + let mut cache = Cache::new(10); + let ttl = Duration::from_millis(50); + + cache.insert_with_ttl("expired1", 1, ttl); + cache.insert_with_ttl("expired2", 2, ttl); + cache.insert("normal", 3); + + assert_eq!(cache.len(), 3); + + // Espera os itens expirarem + thread::sleep(Duration::from_millis(100)); + + // Limpeza manual + let removed_count = cache.cleanup_expired(); + assert_eq!(removed_count, 2); + assert_eq!(cache.len(), 1); + assert_eq!(cache.get("normal"), Some(&3.to_value())); + } + + #[test] + fn test_contains_key_with_expired() { + let mut cache = Cache::new(10); + let ttl = Duration::from_millis(50); + + cache.insert_with_ttl("test", 42, ttl); + assert!(cache.contains_key("test")); + + // Espera expirar + thread::sleep(Duration::from_millis(100)); + assert!(!cache.contains_key("test")); + assert_eq!(cache.len(), 0); + } + + #[test] + fn test_list_filters_expired_items() { + let mut cache = Cache::new(10); + let ttl = Duration::from_millis(50); + + cache.insert_with_ttl("expired", 1, ttl); + cache.insert("normal1", 2); + cache.insert("normal2", 3); + + assert_eq!(cache.len(), 3); + + // Espera um item expirar + thread::sleep(Duration::from_millis(100)); + + // List deve retornar apenas os itens válidos + let result = cache.list(crate::ListProps::default()).unwrap(); + assert_eq!(result.len(), 2); + assert_eq!(cache.len(), 2); // Item expirado foi removido automaticamente + } + + #[test] + fn test_set_default_ttl() { + let mut cache = Cache::new(10); + assert_eq!(cache.get_default_ttl(), None); + + let ttl = Duration::from_secs(60); + cache.set_default_ttl(Some(ttl)); + assert_eq!(cache.get_default_ttl(), Some(ttl)); + + cache.set_default_ttl(None); + assert_eq!(cache.get_default_ttl(), None); + } +}