1use std::num::NonZeroUsize;
2
3use lru::LruCache;
4
5use crate::checker::Diagnostic;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15struct ResultKey {
16 engine: &'static str,
17 language: String,
18 content: u64,
19}
20
21pub struct ResultCache {
34 cache: Option<LruCache<ResultKey, Vec<Diagnostic>>>,
37}
38
39impl ResultCache {
40 #[must_use]
44 pub fn new(capacity: usize) -> Self {
45 Self {
46 cache: NonZeroUsize::new(capacity).map(LruCache::new),
47 }
48 }
49
50 fn key(engine: &'static str, language: &str, text: &str) -> ResultKey {
51 ResultKey {
52 engine,
53 language: language.to_string(),
54 content: crate::hashing::content_hash(text),
55 }
56 }
57
58 #[must_use]
60 pub fn get(
61 &mut self,
62 engine: &'static str,
63 language: &str,
64 text: &str,
65 ) -> Option<Vec<Diagnostic>> {
66 self.cache
67 .as_mut()?
68 .get(&Self::key(engine, language, text))
69 .cloned()
70 }
71
72 pub fn put(
74 &mut self,
75 engine: &'static str,
76 language: &str,
77 text: &str,
78 diagnostics: Vec<Diagnostic>,
79 ) {
80 if let Some(cache) = self.cache.as_mut() {
81 cache.put(Self::key(engine, language, text), diagnostics);
82 }
83 }
84
85 #[must_use]
87 pub fn len(&self) -> usize {
88 self.cache.as_ref().map_or(0, LruCache::len)
89 }
90
91 #[must_use]
92 pub fn is_empty(&self) -> bool {
93 self.len() == 0
94 }
95
96 pub fn clear(&mut self) {
98 if let Some(cache) = self.cache.as_mut() {
99 cache.clear();
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 fn diagnostic(rule: &str) -> Diagnostic {
109 Diagnostic {
110 start_byte: 0,
111 end_byte: 4,
112 message: String::new(),
113 suggestions: Vec::new(),
114 rule_id: rule.to_string(),
115 severity: 2,
116 unified_id: String::new(),
117 confidence: 1.0,
118 language: String::new(),
119 pack_installable: false,
120 }
121 }
122
123 #[test]
124 fn an_answer_comes_back_for_the_same_prose() {
125 let mut cache = ResultCache::new(10);
126 assert!(cache.get("harper", "en-US", "some prose").is_none());
127 cache.put(
128 "harper",
129 "en-US",
130 "some prose",
131 vec![diagnostic("harper.Spelling")],
132 );
133 let hit = cache.get("harper", "en-US", "some prose").expect("hit");
134 assert_eq!(hit[0].rule_id, "harper.Spelling");
135 }
136
137 #[test]
138 fn changed_prose_misses() {
139 let mut cache = ResultCache::new(10);
140 cache.put(
141 "harper",
142 "en-US",
143 "some prose",
144 vec![diagnostic("harper.Spelling")],
145 );
146 assert!(cache.get("harper", "en-US", "some prose edited").is_none());
147 }
148
149 #[test]
150 fn another_engine_or_language_misses() {
151 let mut cache = ResultCache::new(10);
152 cache.put(
153 "harper",
154 "en-US",
155 "some prose",
156 vec![diagnostic("harper.Spelling")],
157 );
158 assert!(cache.get("languagetool", "en-US", "some prose").is_none());
159 assert!(cache.get("harper", "fr", "some prose").is_none());
160 }
161
162 #[test]
163 fn the_oldest_answer_is_evicted_at_capacity() {
164 let mut cache = ResultCache::new(2);
165 cache.put("harper", "en-US", "one", Vec::new());
166 cache.put("harper", "en-US", "two", Vec::new());
167 cache.put("harper", "en-US", "three", Vec::new());
168 assert_eq!(cache.len(), 2);
169 assert!(cache.get("harper", "en-US", "one").is_none());
170 assert!(cache.get("harper", "en-US", "three").is_some());
171 }
172
173 #[test]
174 fn a_zero_capacity_cache_is_switched_off() {
175 let mut cache = ResultCache::new(0);
176 cache.put("harper", "en-US", "one", Vec::new());
177 assert!(cache.get("harper", "en-US", "one").is_none());
178 assert!(cache.is_empty());
179 }
180}