Skip to main content

crates_docs/tools/docs/
cache.rs

1//! 文档缓存模块
2//!
3//! 提供文档专用的缓存服务,支持 crate 文档、搜索结果和项目文档的独立 TTL 配置。
4//!
5//! # 缓存键格式
6//!
7//! - Crate 文档: `crate:{name}` 或 `crate:{name}:{version}`
8//! - 搜索结果: `search:{query}:{limit}`
9//! - 项目文档: `item:{crate}:{path}` 或 `item:{crate}:{version}:{path}`
10//!
11//! # 示例
12//!
13//! ```rust,no_run
14//! use std::sync::Arc;
15//! use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
16//! use crates_docs::cache::memory::MemoryCache;
17//!
18//! let cache = Arc::new(MemoryCache::new(1000));
19//! let doc_cache = DocCache::new(cache);
20//! ```
21
22use crate::cache::Cache;
23use std::sync::Arc;
24use std::time::Duration;
25
26/// 文档缓存 TTL 配置
27///
28/// 为不同类型的文档配置独立的 TTL。
29///
30/// # 字段
31///
32/// - `crate_docs_secs`: crate 文档缓存时间(秒)
33/// - `search_results_secs`: 搜索结果缓存时间(秒)
34/// - `item_docs_secs`: 项目文档缓存时间(秒)
35#[derive(Debug, Clone, Copy)]
36pub struct DocCacheTtl {
37    /// crate 文档 TTL(秒)
38    pub crate_docs_secs: u64,
39    /// 搜索结果 TTL(秒)
40    pub search_results_secs: u64,
41    /// 项目文档 TTL(秒)
42    pub item_docs_secs: u64,
43}
44
45impl Default for DocCacheTtl {
46    fn default() -> Self {
47        Self {
48            crate_docs_secs: 3600,    // 1 小时
49            search_results_secs: 300, // 5 分钟
50            item_docs_secs: 1800,     // 30 分钟
51        }
52    }
53}
54
55impl DocCacheTtl {
56    /// 从 `CacheConfig` 创建 TTL 配置
57    ///
58    /// # 参数
59    ///
60    /// * `config` - 缓存配置
61    ///
62    /// # 返回值
63    ///
64    /// 返回根据配置创建的 TTL 配置
65    #[must_use]
66    pub fn from_cache_config(config: &crate::cache::CacheConfig) -> Self {
67        Self {
68            crate_docs_secs: config.crate_docs_ttl_secs.unwrap_or(3600),
69            search_results_secs: config.search_results_ttl_secs.unwrap_or(300),
70            item_docs_secs: config.item_docs_ttl_secs.unwrap_or(1800),
71        }
72    }
73}
74
75/// 文档缓存服务
76///
77/// 提供文档专用的缓存操作,支持 crate 文档、搜索结果和项目文档。
78///
79/// # 字段
80///
81/// - `cache`: 底层缓存实例
82/// - `ttl`: TTL 配置
83#[derive(Clone)]
84pub struct DocCache {
85    cache: Arc<dyn Cache>,
86    ttl: DocCacheTtl,
87}
88
89impl DocCache {
90    /// 创建新的文档缓存(使用默认 TTL)
91    ///
92    /// # 参数
93    ///
94    /// * `cache` - 缓存实例
95    ///
96    /// # 示例
97    ///
98    /// ```rust,no_run
99    /// use std::sync::Arc;
100    /// use crates_docs::tools::docs::cache::DocCache;
101    /// use crates_docs::cache::memory::MemoryCache;
102    ///
103    /// let cache = Arc::new(MemoryCache::new(1000));
104    /// let doc_cache = DocCache::new(cache);
105    /// ```
106    pub fn new(cache: Arc<dyn Cache>) -> Self {
107        Self {
108            cache,
109            ttl: DocCacheTtl::default(),
110        }
111    }
112
113    /// 创建新的文档缓存(使用自定义 TTL)
114    ///
115    /// # 参数
116    ///
117    /// * `cache` - 缓存实例
118    /// * `ttl` - TTL 配置
119    ///
120    /// # 示例
121    ///
122    /// ```rust,no_run
123    /// use std::sync::Arc;
124    /// use crates_docs::tools::docs::cache::{DocCache, DocCacheTtl};
125    /// use crates_docs::cache::memory::MemoryCache;
126    ///
127    /// let cache = Arc::new(MemoryCache::new(1000));
128    /// let ttl = DocCacheTtl {
129    ///     crate_docs_secs: 7200,
130    ///     search_results_secs: 600,
131    ///     item_docs_secs: 3600,
132    /// };
133    /// let doc_cache = DocCache::with_ttl(cache, ttl);
134    /// ```
135    #[must_use]
136    pub fn with_ttl(cache: Arc<dyn Cache>, ttl: DocCacheTtl) -> Self {
137        Self { cache, ttl }
138    }
139
140    /// 获取缓存的 crate 文档
141    ///
142    /// # 参数
143    ///
144    /// * `crate_name` - crate 名称
145    /// * `version` - 可选的版本号
146    ///
147    /// # 返回值
148    ///
149    /// 如果缓存命中,返回文档内容;否则返回 `None`
150    pub async fn get_crate_docs(&self, crate_name: &str, version: Option<&str>) -> Option<String> {
151        let key = Self::crate_cache_key(crate_name, version);
152        self.cache.get(&key).await
153    }
154
155    /// 设置 crate 文档缓存
156    ///
157    /// # 参数
158    ///
159    /// * `crate_name` - crate 名称
160    /// * `version` - 可选的版本号
161    /// * `content` - 文档内容
162    ///
163    /// # 错误
164    ///
165    /// 如果缓存操作失败,返回错误
166    pub async fn set_crate_docs(
167        &self,
168        crate_name: &str,
169        version: Option<&str>,
170        content: String,
171    ) -> crate::error::Result<()> {
172        let key = Self::crate_cache_key(crate_name, version);
173        self.cache
174            .set(
175                key,
176                content,
177                Some(Duration::from_secs(self.ttl.crate_docs_secs)),
178            )
179            .await
180    }
181
182    /// 获取缓存的搜索结果
183    ///
184    /// # 参数
185    ///
186    /// * `query` - 搜索查询
187    /// * `limit` - 结果数量限制
188    ///
189    /// # 返回值
190    ///
191    /// 如果缓存命中,返回搜索结果;否则返回 `None`
192    pub async fn get_search_results(&self, query: &str, limit: u32) -> Option<String> {
193        let key = Self::search_cache_key(query, limit);
194        self.cache.get(&key).await
195    }
196
197    /// 设置搜索结果缓存
198    ///
199    /// # 参数
200    ///
201    /// * `query` - 搜索查询
202    /// * `limit` - 结果数量限制
203    /// * `content` - 搜索结果内容
204    ///
205    /// # 错误
206    ///
207    /// 如果缓存操作失败,返回错误
208    pub async fn set_search_results(
209        &self,
210        query: &str,
211        limit: u32,
212        content: String,
213    ) -> crate::error::Result<()> {
214        let key = Self::search_cache_key(query, limit);
215        self.cache
216            .set(
217                key,
218                content,
219                Some(Duration::from_secs(self.ttl.search_results_secs)),
220            )
221            .await
222    }
223
224    /// 获取缓存的项目文档
225    ///
226    /// # 参数
227    ///
228    /// * `crate_name` - crate 名称
229    /// * `item_path` - 项目路径
230    /// * `version` - 可选的版本号
231    ///
232    /// # 返回值
233    ///
234    /// 如果缓存命中,返回项目文档;否则返回 `None`
235    pub async fn get_item_docs(
236        &self,
237        crate_name: &str,
238        item_path: &str,
239        version: Option<&str>,
240    ) -> Option<String> {
241        let key = Self::item_cache_key(crate_name, item_path, version);
242        self.cache.get(&key).await
243    }
244
245    /// 设置项目文档缓存
246    ///
247    /// # 参数
248    ///
249    /// * `crate_name` - crate 名称
250    /// * `item_path` - 项目路径
251    /// * `version` - 可选的版本号
252    /// * `content` - 文档内容
253    ///
254    /// # 错误
255    ///
256    /// 如果缓存操作失败,返回错误
257    pub async fn set_item_docs(
258        &self,
259        crate_name: &str,
260        item_path: &str,
261        version: Option<&str>,
262        content: String,
263    ) -> crate::error::Result<()> {
264        let key = Self::item_cache_key(crate_name, item_path, version);
265        self.cache
266            .set(
267                key,
268                content,
269                Some(Duration::from_secs(self.ttl.item_docs_secs)),
270            )
271            .await
272    }
273
274    /// 清除缓存
275    ///
276    /// # 错误
277    ///
278    /// 如果缓存操作失败,返回错误
279    pub async fn clear(&self) -> crate::error::Result<()> {
280        self.cache.clear().await
281    }
282
283    /// 构建 crate 缓存键
284    fn crate_cache_key(crate_name: &str, version: Option<&str>) -> String {
285        if let Some(ver) = version {
286            format!("crate:{crate_name}:{ver}")
287        } else {
288            format!("crate:{crate_name}")
289        }
290    }
291
292    /// 构建搜索缓存键
293    fn search_cache_key(query: &str, limit: u32) -> String {
294        format!("search:{query}:{limit}")
295    }
296
297    /// 构建项目缓存键
298    fn item_cache_key(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
299        if let Some(ver) = version {
300            format!("item:{crate_name}:{ver}:{item_path}")
301        } else {
302            format!("item:{crate_name}:{item_path}")
303        }
304    }
305}
306
307impl Default for DocCache {
308    fn default() -> Self {
309        let cache = Arc::new(crate::cache::memory::MemoryCache::new(1000));
310        Self::new(cache)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::cache::memory::MemoryCache;
318
319    #[tokio::test]
320    async fn test_doc_cache() {
321        let memory_cache = MemoryCache::new(100);
322        let cache = Arc::new(memory_cache);
323        let doc_cache = DocCache::new(cache);
324
325        // 测试 crate 文档缓存
326        doc_cache
327            .set_crate_docs("serde", Some("1.0"), "Test docs".to_string())
328            .await
329            .expect("set_crate_docs should succeed");
330        let cached = doc_cache.get_crate_docs("serde", Some("1.0")).await;
331        assert_eq!(cached, Some("Test docs".to_string()));
332
333        // 测试搜索结果缓存
334        doc_cache
335            .set_search_results("web framework", 10, "Search results".to_string())
336            .await
337            .expect("set_search_results should succeed");
338        let search_cached = doc_cache.get_search_results("web framework", 10).await;
339        assert_eq!(search_cached, Some("Search results".to_string()));
340
341        // 测试项目文档缓存
342        doc_cache
343            .set_item_docs(
344                "serde",
345                "serde::Serialize",
346                Some("1.0"),
347                "Item docs".to_string(),
348            )
349            .await
350            .expect("set_item_docs should succeed");
351        let item_cached = doc_cache
352            .get_item_docs("serde", "serde::Serialize", Some("1.0"))
353            .await;
354        assert_eq!(item_cached, Some("Item docs".to_string()));
355
356        // 测试清理
357        doc_cache.clear().await.expect("clear should succeed");
358        let cleared = doc_cache.get_crate_docs("serde", Some("1.0")).await;
359        assert_eq!(cleared, None);
360    }
361
362    #[test]
363    fn test_cache_key_generation() {
364        assert_eq!(DocCache::crate_cache_key("serde", None), "crate:serde");
365        assert_eq!(
366            DocCache::crate_cache_key("serde", Some("1.0")),
367            "crate:serde:1.0"
368        );
369
370        assert_eq!(
371            DocCache::search_cache_key("web framework", 10),
372            "search:web framework:10"
373        );
374
375        assert_eq!(
376            DocCache::item_cache_key("serde", "Serialize", None),
377            "item:serde:Serialize"
378        );
379        assert_eq!(
380            DocCache::item_cache_key("serde", "Serialize", Some("1.0")),
381            "item:serde:1.0:Serialize"
382        );
383    }
384
385    #[test]
386    fn test_doc_cache_ttl_default() {
387        let ttl = DocCacheTtl::default();
388        assert_eq!(ttl.crate_docs_secs, 3600);
389        assert_eq!(ttl.search_results_secs, 300);
390        assert_eq!(ttl.item_docs_secs, 1800);
391    }
392
393    #[test]
394    fn test_doc_cache_ttl_from_config() {
395        let config = crate::cache::CacheConfig {
396            cache_type: "memory".to_string(),
397            memory_size: Some(1000),
398            redis_url: None,
399            key_prefix: String::new(),
400            default_ttl: Some(3600),
401            crate_docs_ttl_secs: Some(7200),
402            item_docs_ttl_secs: Some(3600),
403            search_results_ttl_secs: Some(600),
404        };
405        let ttl = DocCacheTtl::from_cache_config(&config);
406        assert_eq!(ttl.crate_docs_secs, 7200);
407        assert_eq!(ttl.item_docs_secs, 3600);
408        assert_eq!(ttl.search_results_secs, 600);
409    }
410}