Skip to main content

crates_docs/tools/docs/cache/
mod.rs

1//! Document cache module
2//!
3//! Provides document-specific cache service with support for independent TTL configuration
4//! for crate docs, search results, and item docs.
5//!
6//! # Cache key format
7//!
8//! - Crate documentation: `crate:{name}` or `crate:{name}:{version}`
9//! - Search results: `search:{query}:{limit}`
10//! - Item documentation: `item:{crate}:{path}` or `item:{crate}:{version}:{path}`
11//!
12//! # Examples
13//!
14//! ```rust,no_run
15//! use std::sync::Arc;
16//! use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
17//! use crates_docs::cache::memory::MemoryCache;
18//!
19//! let cache = Arc::new(MemoryCache::new(1000));
20//! let doc_cache = DocCache::new(cache);
21//! ```
22
23mod key;
24mod stats;
25mod ttl;
26
27use crate::cache::Cache;
28use std::sync::Arc;
29
30// Re-export public types
31pub use key::CacheKeyGenerator;
32pub use stats::CacheStats;
33pub use ttl::DocCacheTtl;
34
35/// Document cache service
36///
37/// Provides document-specific cache operations, supports crate docs, search results, and item docs.
38///
39/// # Fields
40///
41/// - `cache`: Underlying cache instance
42/// - `ttl`: TTL configuration
43/// - `stats`: Cache statistics
44#[derive(Clone)]
45pub struct DocCache {
46    cache: Arc<dyn Cache>,
47    ttl: DocCacheTtl,
48    stats: CacheStats,
49}
50
51impl DocCache {
52    /// Create new document cache (with default TTL)
53    ///
54    /// # Arguments
55    ///
56    /// * `cache` - cache instance
57    ///
58    /// # Examples
59    ///
60    /// ```rust,no_run
61    /// use std::sync::Arc;
62    /// use crates_docs::tools::docs::cache::DocCache;
63    /// use crates_docs::cache::memory::MemoryCache;
64    ///
65    /// let cache = Arc::new(MemoryCache::new(1000));
66    /// let doc_cache = DocCache::new(cache);
67    /// ```
68    pub fn new(cache: Arc<dyn Cache>) -> Self {
69        Self {
70            cache,
71            ttl: DocCacheTtl::default(),
72            stats: CacheStats::new(),
73        }
74    }
75
76    /// Create new document cache (with custom TTL)
77    ///
78    /// # Arguments
79    ///
80    /// * `cache` - cache instance
81    /// * `ttl` - TTL configuration
82    ///
83    /// # Examples
84    ///
85    /// ```rust,no_run
86    /// use std::sync::Arc;
87    /// use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
88    /// use crates_docs::cache::memory::MemoryCache;
89    ///
90    /// let cache = Arc::new(MemoryCache::new(1000));
91    /// let ttl = DocCacheTtl {
92    ///     crate_docs_secs: 7200,
93    ///     search_results_secs: 600,
94    ///     item_docs_secs: 3600,
95    ///     jitter_ratio: 0.1,
96    /// };
97    /// let doc_cache = DocCache::with_ttl(cache, ttl);
98    /// ```
99    #[must_use]
100    pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
101        Self {
102            cache,
103            ttl,
104            stats: CacheStats::new(),
105        }
106    }
107
108    /// Get cached crate documentation
109    ///
110    /// # Arguments
111    ///
112    /// * `crate_name` - crate name
113    /// * `version` - Optional version
114    ///
115    /// # Returns
116    ///
117    /// Returns document content if cache hit; otherwise returns `None`
118    pub async fn get_crate_docs(&self, crate_name: &str, version: Option<&str>) -> Option<String> {
119        let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
120        let result = self.cache.get(&key).await;
121        if result.is_some() {
122            self.stats.record_hit();
123        } else {
124            self.stats.record_miss();
125        }
126        result
127    }
128
129    /// Set crate document cache
130    ///
131    /// # Arguments
132    ///
133    /// * `crate_name` - crate name
134    /// * `version` - Optional version
135    /// * `content` - Document content
136    ///
137    /// # Errors
138    ///
139    /// Returns error if cache operation fails
140    pub async fn set_crate_docs(
141        &self,
142        crate_name: &str,
143        version: Option<&str>,
144        content: String,
145    ) -> crate::error::Result<()> {
146        let key = CacheKeyGenerator::crate_cache_key(crate_name, version);
147        let ttl = self.ttl.crate_docs_duration();
148        self.cache.set(key, content, Some(ttl)).await?;
149        self.stats.record_set();
150        Ok(())
151    }
152
153    /// Get cached search results
154    ///
155    /// # Arguments
156    ///
157    /// * `query` - Search query
158    /// * `limit` - Result count limit
159    ///
160    /// # Returns
161    ///
162    /// Returns search results if cache hit;otherwise returns `None`
163    pub async fn get_search_results(&self, query: &str, limit: u32) -> Option<String> {
164        let key = CacheKeyGenerator::search_cache_key(query, limit);
165        let result = self.cache.get(&key).await;
166        if result.is_some() {
167            self.stats.record_hit();
168        } else {
169            self.stats.record_miss();
170        }
171        result
172    }
173
174    /// Set search results cache
175    ///
176    /// # Arguments
177    ///
178    /// * `query` - Search query
179    /// * `limit` - Result count limit
180    /// * `content` - search result content
181    ///
182    /// # Errors
183    ///
184    /// Returns error if cache operation fails
185    pub async fn set_search_results(
186        &self,
187        query: &str,
188        limit: u32,
189        content: String,
190    ) -> crate::error::Result<()> {
191        let key = CacheKeyGenerator::search_cache_key(query, limit);
192        let ttl = self.ttl.search_results_duration();
193        self.cache.set(key, content, Some(ttl)).await?;
194        self.stats.record_set();
195        Ok(())
196    }
197
198    /// Get cached item docs
199    ///
200    /// # Arguments
201    ///
202    /// * `crate_name` - crate name
203    /// * `item_path` - Item path
204    /// * `version` - Optional version
205    ///
206    /// # Returns
207    ///
208    /// Returns item docs if cache hit;otherwise returns `None`
209    pub async fn get_item_docs(
210        &self,
211        crate_name: &str,
212        item_path: &str,
213        version: Option<&str>,
214    ) -> Option<String> {
215        let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
216        let result = self.cache.get(&key).await;
217        if result.is_some() {
218            self.stats.record_hit();
219        } else {
220            self.stats.record_miss();
221        }
222        result
223    }
224
225    /// Set item docs cache
226    ///
227    /// # Arguments
228    ///
229    /// * `crate_name` - crate name
230    /// * `item_path` - Item path
231    /// * `version` - Optional version
232    /// * `content` - Document content
233    ///
234    /// # Errors
235    ///
236    /// Returns error if cache operation fails
237    pub async fn set_item_docs(
238        &self,
239        crate_name: &str,
240        item_path: &str,
241        version: Option<&str>,
242        content: String,
243    ) -> crate::error::Result<()> {
244        let key = CacheKeyGenerator::item_cache_key(crate_name, item_path, version);
245        let ttl = self.ttl.item_docs_duration();
246        self.cache.set(key, content, Some(ttl)).await?;
247        self.stats.record_set();
248        Ok(())
249    }
250
251    /// Clear cache
252    ///
253    /// # Errors
254    ///
255    /// Returns error if cache operation fails
256    pub async fn clear(&self) -> crate::error::Result<()> {
257        self.cache.clear().await
258    }
259
260    /// Get cache statistics
261    #[must_use]
262    pub fn stats(&self) -> &CacheStats {
263        &self.stats
264    }
265
266    /// Get TTL configuration
267    #[must_use]
268    pub fn ttl(&self) -> &DocCacheTtl {
269        &self.ttl
270    }
271}
272
273impl Default for DocCache {
274    fn default() -> Self {
275        let cache = Arc::new(crate::cache::memory::MemoryCache::new(1000));
276        Self::new(cache)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::cache::memory::MemoryCache;
284
285    #[tokio::test]
286    async fn test_doc_cache() {
287        let memory_cache = MemoryCache::new(100);
288        let cache = Arc::new(memory_cache);
289        let doc_cache = DocCache::new(cache);
290
291        // Test crate document cache
292        doc_cache
293            .set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
294            .await
295            .expect("set_crate_docs should succeed");
296        let cached = doc_cache.get_crate_docs("serde", Some("1.0")).await;
297        assert_eq!(cached, Some("Test docs".to_string()));
298
299        // Test search results cache
300        doc_cache
301            .set_search_results("web framework", 10, "Search results".to_string())
302            .await
303            .expect("set_search_results should succeed");
304        let search_cached = doc_cache.get_search_results("web framework", 10).await;
305        assert_eq!(search_cached, Some("Search results".to_string()));
306
307        // Test item docs cache
308        doc_cache
309            .set_item_docs(
310                "serde",
311                "serde::Serialize",
312                Some("1.0"),
313                "Item docs".to_string(),
314            )
315            .await
316            .expect("set_item_docs should succeed");
317        let item_cached = doc_cache
318            .get_item_docs("serde", "serde::Serialize", Some("1.0"))
319            .await;
320        assert_eq!(item_cached, Some("Item docs".to_string()));
321
322        // Test clear
323        doc_cache.clear().await.expect("clear should succeed");
324        let cleared = doc_cache.get_crate_docs("serde", Some("1.0")).await;
325        assert_eq!(cleared, None);
326    }
327
328    #[tokio::test]
329    async fn test_doc_cache_with_ttl() {
330        let memory_cache = MemoryCache::new(100);
331        let cache = Arc::new(memory_cache);
332
333        let ttl = DocCacheTtl {
334            crate_docs_secs: 7200,
335            search_results_secs: 600,
336            item_docs_secs: 3600,
337            jitter_ratio: 0.0, // Disable jitter for predictable tests
338        };
339
340        let doc_cache = DocCache::with_ttl(cache, ttl);
341
342        assert_eq!(doc_cache.ttl().crate_docs_secs, 7200);
343        assert_eq!(doc_cache.ttl().search_results_secs, 600);
344        assert_eq!(doc_cache.ttl().item_docs_secs, 3600);
345    }
346
347    #[tokio::test]
348    async fn test_doc_cache_stats() {
349        let memory_cache = MemoryCache::new(100);
350        let cache = Arc::new(memory_cache);
351        let doc_cache = DocCache::new(cache);
352
353        // Record a hit
354        doc_cache
355            .set_crate_docs("serde", None, "docs".to_string())
356            .await
357            .ok();
358        doc_cache.get_crate_docs("serde", None).await;
359
360        // Record a miss
361        doc_cache.get_crate_docs("nonexistent", None).await;
362
363        assert_eq!(doc_cache.stats().hits(), 1);
364        assert_eq!(doc_cache.stats().misses(), 1);
365        assert_eq!(doc_cache.stats().sets(), 1);
366    }
367
368    #[test]
369    fn test_doc_cache_default() {
370        let doc_cache = DocCache::default();
371        assert_eq!(doc_cache.ttl().crate_docs_secs, 3600);
372        assert_eq!(doc_cache.ttl().search_results_secs, 300);
373        assert_eq!(doc_cache.ttl().item_docs_secs, 1800);
374    }
375}