flag_rs/
completion_cache.rs1use crate::completion::CompletionResult;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant};
10
11#[derive(Clone)]
13struct CacheEntry {
14 result: CompletionResult,
15 timestamp: Instant,
16}
17
18pub struct CompletionCache {
23 cache: Arc<Mutex<HashMap<String, CacheEntry>>>,
24 ttl: Duration,
25}
26
27impl CompletionCache {
28 pub fn new(ttl: Duration) -> Self {
34 Self {
35 cache: Arc::new(Mutex::new(HashMap::new())),
36 ttl,
37 }
38 }
39
40 pub fn with_default_ttl() -> Self {
42 Self::new(Duration::from_secs(5))
43 }
44
45 pub fn make_key(
50 command_path: &[String],
51 prefix: &str,
52 flags: &HashMap<String, String>,
53 ) -> String {
54 let mut parts = vec![];
55
56 parts.extend(command_path.iter().cloned());
58
59 parts.push(format!("__prefix:{prefix}"));
61
62 let mut flag_parts: Vec<String> = flags.iter().map(|(k, v)| format!("{k}={v}")).collect();
64 flag_parts.sort(); parts.extend(flag_parts);
66
67 parts.join(":")
68 }
69
70 pub fn get(&self, key: &str) -> Option<CompletionResult> {
75 let mut cache = self.cache.lock().ok()?;
76
77 if let Some(entry) = cache.get(key) {
78 if entry.timestamp.elapsed() < self.ttl {
79 return Some(entry.result.clone());
80 }
81 cache.remove(key);
83 }
84
85 None
86 }
87
88 pub fn put(&self, key: String, result: CompletionResult) {
95 if let Ok(mut cache) = self.cache.lock() {
96 cache.insert(
97 key,
98 CacheEntry {
99 result,
100 timestamp: Instant::now(),
101 },
102 );
103
104 self.cleanup_expired(&mut cache);
106 }
107 }
108
109 fn cleanup_expired(&self, cache: &mut HashMap<String, CacheEntry>) {
111 let now = Instant::now();
112 cache.retain(|_, entry| now.duration_since(entry.timestamp) < self.ttl);
113 }
114
115 pub fn clear(&self) {
117 if let Ok(mut cache) = self.cache.lock() {
118 cache.clear();
119 }
120 }
121
122 pub fn size(&self) -> usize {
124 self.cache.lock().map_or(0, |c| c.len())
125 }
126}
127
128impl Default for CompletionCache {
129 fn default() -> Self {
130 Self::with_default_ttl()
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::completion::CompletionResult;
138
139 #[test]
140 fn test_cache_basic_operations() {
141 let cache = CompletionCache::new(Duration::from_secs(1));
142 let key = "test:key";
143 let result = CompletionResult::new().add("item1").add("item2");
144
145 assert!(cache.get(key).is_none());
147
148 cache.put(key.to_string(), result.clone());
150 let cached = cache.get(key).unwrap();
151 assert_eq!(cached.values, result.values);
152
153 std::thread::sleep(Duration::from_millis(1100));
155 assert!(cache.get(key).is_none());
156 }
157
158 #[test]
159 fn test_cache_key_generation() {
160 let mut flags = HashMap::new();
161 flags.insert("namespace".to_string(), "default".to_string());
162 flags.insert("verbose".to_string(), "true".to_string());
163
164 let key1 =
165 CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
166 let key2 =
167 CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
168 assert_eq!(key1, key2);
169
170 let key3 =
172 CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "pod", &flags);
173 assert_ne!(key1, key3);
174
175 flags.insert("all-namespaces".to_string(), "true".to_string());
177 let key4 =
178 CompletionCache::make_key(&["kubectl".to_string(), "get".to_string()], "po", &flags);
179 assert_ne!(key1, key4);
180 }
181
182 #[test]
183 fn test_cache_cleanup() {
184 let cache = CompletionCache::new(Duration::from_millis(100));
185
186 for i in 0..5 {
188 let key = format!("key{i}");
189 let result = CompletionResult::new().add(format!("item{i}"));
190 cache.put(key, result);
191 }
192
193 assert_eq!(cache.size(), 5);
194
195 std::thread::sleep(Duration::from_millis(150));
197
198 cache.put("new".to_string(), CompletionResult::new());
200
201 assert_eq!(cache.size(), 1);
203 }
204}