Skip to main content

java_manager/
cache.rs

1//! TTL-based cache for Java search results.
2
3use crate::{JavaError, JavaInfo};
4use std::time::{Duration, Instant};
5
6const DEFAULT_TTL: Duration = Duration::from_secs(300);
7
8/// A simple TTL-based cache for Java search results.
9///
10/// Java installations rarely change during a session, so caching
11/// avoids repeated full-disk scans. Call [`get_or_refresh`] to
12/// retrieve cached results or run a fetcher when the TTL expires.
13///
14/// [`get_or_refresh`]: JavaCache::get_or_refresh
15#[derive(Debug)]
16pub struct JavaCache {
17    results: Vec<JavaInfo>,
18    cached_at: Option<Instant>,
19    ttl: Duration,
20}
21
22impl Default for JavaCache {
23    fn default() -> Self {
24        Self::new(DEFAULT_TTL)
25    }
26}
27
28impl JavaCache {
29    /// Create a new cache with a given TTL.
30    ///
31    /// After `ttl` elapses, the next call to [`get_or_refresh`]
32    /// will run the fetcher again.
33    ///
34    /// [`get_or_refresh`]: JavaCache::get_or_refresh
35    pub fn new(ttl: Duration) -> Self {
36        Self {
37            results: Vec::new(),
38            cached_at: None,
39            ttl,
40        }
41    }
42
43    /// Set a custom TTL (builder-style).
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use java_manager::JavaCache;
49    /// use std::time::Duration;
50    ///
51    /// let cache = JavaCache::new(Duration::from_secs(60)).ttl(Duration::from_secs(30));
52    /// ```
53    pub fn ttl(mut self, ttl: Duration) -> Self {
54        self.ttl = ttl;
55        self
56    }
57
58    /// Return cached results if they are still fresh, otherwise
59    /// run `fetcher` and cache the new results.
60    ///
61    /// # Errors
62    ///
63    /// Propagates any error returned by `fetcher`.
64    ///
65    /// # Examples
66    ///
67    /// ```no_run
68    /// use java_manager::{JavaCache, full_search};
69    /// use std::time::Duration;
70    ///
71    /// let mut cache = JavaCache::new(Duration::from_secs(300));
72    /// let javas = cache.get_or_refresh(|| full_search())?;
73    /// println!("Found {} Java(s)", javas.len());
74    /// # Ok::<_, java_manager::JavaError>(())
75    /// ```
76    pub fn get_or_refresh<F>(&mut self, fetcher: F) -> Result<&[JavaInfo], JavaError>
77    where
78        F: Fn() -> Result<Vec<JavaInfo>, JavaError>,
79    {
80        if self.is_fresh() {
81            log::debug!(
82                "JavaCache: returning {} cached result(s)",
83                self.results.len()
84            );
85            return Ok(&self.results);
86        }
87
88        log::debug!("JavaCache: cache expired, fetching...");
89        self.results = fetcher()?;
90        self.cached_at = Some(Instant::now());
91        Ok(&self.results)
92    }
93
94    /// Force a refresh, ignoring the TTL.
95    ///
96    /// # Errors
97    ///
98    /// Propagates any error returned by `fetcher`.
99    ///
100    /// # Examples
101    ///
102    /// ```no_run
103    /// use java_manager::{JavaCache, full_search};
104    /// use std::time::Duration;
105    ///
106    /// let mut cache = JavaCache::new(Duration::from_secs(300));
107    /// let javas = cache.force_refresh(|| full_search())?;
108    /// println!("Found {} Java(s)", javas.len());
109    /// # Ok::<_, java_manager::JavaError>(())
110    /// ```
111    pub fn force_refresh<F>(&mut self, fetcher: F) -> Result<&[JavaInfo], JavaError>
112    where
113        F: Fn() -> Result<Vec<JavaInfo>, JavaError>,
114    {
115        log::debug!("JavaCache: force refresh");
116        self.results = fetcher()?;
117        self.cached_at = Some(Instant::now());
118        Ok(&self.results)
119    }
120
121    /// Clear the cache.
122    ///
123    /// After calling `clear()`, the next [`get_or_refresh`](JavaCache::get_or_refresh)
124    /// call will always run the fetcher regardless of the TTL.
125    pub fn clear(&mut self) {
126        self.results.clear();
127        self.cached_at = None;
128    }
129
130    fn is_fresh(&self) -> bool {
131        self.cached_at.is_some_and(|t| t.elapsed() < self.ttl)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    fn dummy_fetcher(count: usize) -> impl Fn() -> Result<Vec<JavaInfo>, JavaError> {
140        move || {
141            let mut results = Vec::new();
142            for i in 0..count {
143                results.push(JavaInfo {
144                    name: format!("Java {}", i),
145                    version: format!("{}.0.0", 8 + i),
146                    ..Default::default()
147                });
148            }
149            Ok(results)
150        }
151    }
152
153    #[test]
154    fn test_cache_initial_miss() {
155        let mut cache = JavaCache::new(Duration::from_secs(60));
156        let results = cache.get_or_refresh(dummy_fetcher(2)).unwrap();
157        assert_eq!(results.len(), 2);
158    }
159
160    #[test]
161    fn test_cache_hit_within_ttl() {
162        let mut cache = JavaCache::new(Duration::from_secs(60));
163        let _ = cache.get_or_refresh(dummy_fetcher(1)).unwrap();
164        let results = cache.get_or_refresh(dummy_fetcher(999)).unwrap();
165        assert_eq!(results.len(), 1);
166    }
167
168    #[test]
169    fn test_force_refresh() {
170        let mut cache = JavaCache::new(Duration::from_secs(60));
171        let _ = cache.get_or_refresh(dummy_fetcher(1)).unwrap();
172        let results = cache.force_refresh(dummy_fetcher(3)).unwrap();
173        assert_eq!(results.len(), 3);
174    }
175
176    #[test]
177    fn test_clear() {
178        let mut cache = JavaCache::new(Duration::from_secs(60));
179        let _ = cache.get_or_refresh(dummy_fetcher(1)).unwrap();
180        cache.clear();
181        assert!(cache.cached_at.is_none());
182    }
183
184    #[test]
185    fn test_default_ttl() {
186        let cache = JavaCache::default();
187        assert_eq!(cache.ttl, DEFAULT_TTL);
188    }
189
190    #[test]
191    fn test_custom_ttl_builder() {
192        let cache = JavaCache::new(Duration::from_secs(10)).ttl(Duration::from_secs(30));
193        assert_eq!(cache.ttl, Duration::from_secs(30));
194    }
195
196    #[test]
197    fn test_cache_fetcher_error() {
198        let mut cache = JavaCache::new(Duration::from_secs(60));
199        let result: Result<&[JavaInfo], JavaError> =
200            cache.get_or_refresh(|| Err(JavaError::Other("fetch failed".into())));
201        assert!(result.is_err());
202        assert!(result.unwrap_err().to_string().contains("fetch failed"));
203    }
204
205    #[test]
206    fn test_cache_ttl_zero() {
207        let mut cache = JavaCache::new(Duration::ZERO);
208        let r1 = cache.get_or_refresh(dummy_fetcher(1)).unwrap();
209        assert_eq!(r1.len(), 1);
210        // TTL is zero → second call should re-fetch
211        let r2 = cache.get_or_refresh(dummy_fetcher(2)).unwrap();
212        assert_eq!(r2.len(), 2);
213    }
214}