agpm_cli/templating/
cache.rs1use std::collections::HashMap;
7
8use crate::core::ResourceType;
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22pub(crate) struct RenderCacheKey {
23 pub(crate) resource_path: String,
25 pub(crate) resource_type: ResourceType,
27 pub(crate) tool: Option<String>,
30 pub(crate) variant_inputs_hash: String,
32 pub(crate) resolved_commit: Option<String>,
35 pub(crate) dependency_hash: String,
37}
38
39impl RenderCacheKey {
40 #[must_use]
42 pub(crate) fn new(
43 resource_path: String,
44 resource_type: ResourceType,
45 tool: Option<String>,
46 variant_inputs_hash: String,
47 resolved_commit: Option<String>,
48 dependency_hash: String,
49 ) -> Self {
50 Self {
51 resource_path,
52 resource_type,
53 tool,
54 variant_inputs_hash,
55 resolved_commit,
56 dependency_hash,
57 }
58 }
59}
60
61#[derive(Debug, Default)]
79pub(crate) struct RenderCache {
80 cache: HashMap<RenderCacheKey, String>,
82 hits: usize,
84 misses: usize,
85}
86
87impl RenderCache {
88 #[must_use]
90 pub(crate) fn new() -> Self {
91 Self {
92 cache: HashMap::new(),
93 hits: 0,
94 misses: 0,
95 }
96 }
97
98 pub(crate) fn get(&mut self, key: &RenderCacheKey) -> Option<&String> {
100 if let Some(content) = self.cache.get(key) {
101 self.hits += 1;
102 Some(content)
103 } else {
104 self.misses += 1;
105 None
106 }
107 }
108
109 pub(crate) fn insert(&mut self, key: RenderCacheKey, content: String) {
111 self.cache.insert(key, content);
112 }
113
114 pub(crate) fn clear(&mut self) {
116 self.cache.clear();
117 self.hits = 0;
118 self.misses = 0;
119 }
120
121 #[must_use]
123 pub(crate) fn stats(&self) -> (usize, usize) {
124 (self.hits, self.misses)
125 }
126
127 #[must_use]
129 pub(crate) fn hit_rate(&self) -> f64 {
130 let total = self.hits + self.misses;
131 if total == 0 {
132 0.0
133 } else {
134 (self.hits as f64 / total as f64) * 100.0
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_render_cache_key_includes_commit() {
145 let key1 = RenderCacheKey::new(
147 "agents/helper.md".to_string(),
148 ResourceType::Agent,
149 Some("claude-code".to_string()),
150 "hash123".to_string(),
151 Some("abc123def".to_string()),
152 "dep_hash_123".to_string(),
153 );
154
155 let key2 = RenderCacheKey::new(
156 "agents/helper.md".to_string(),
157 ResourceType::Agent,
158 Some("claude-code".to_string()),
159 "hash123".to_string(),
160 Some("def456ghi".to_string()),
161 "dep_hash_456".to_string(),
162 );
163
164 assert_ne!(key1, key2, "Different commits should have different cache keys");
165
166 let key3 = RenderCacheKey::new(
168 "agents/helper.md".to_string(),
169 ResourceType::Agent,
170 Some("claude-code".to_string()),
171 "hash123".to_string(),
172 Some("abc123def".to_string()),
173 "dep_hash_123".to_string(),
174 );
175
176 assert_eq!(key1, key3, "Same commits should have identical cache keys");
177
178 let key4 = RenderCacheKey::new(
180 "agents/helper.md".to_string(),
181 ResourceType::Agent,
182 Some("claude-code".to_string()),
183 "hash123".to_string(),
184 None,
185 "dep_hash_none".to_string(),
186 );
187
188 assert_ne!(key1, key4, "Some(commit) vs None should have different cache keys");
189 }
190
191 #[test]
192 fn test_render_cache_basic_operations() {
193 let mut cache = RenderCache::new();
194 let key = RenderCacheKey::new(
195 "test.md".to_string(),
196 ResourceType::Snippet,
197 Some("claude-code".to_string()),
198 "hash789".to_string(),
199 Some("commit123".to_string()),
200 "dep_hash_test".to_string(),
201 );
202
203 assert_eq!(cache.stats(), (0, 0));
205 assert_eq!(cache.hit_rate(), 0.0);
206
207 assert!(cache.get(&key).is_none());
209 assert_eq!(cache.stats(), (0, 1));
210
211 cache.insert(key.clone(), "rendered content".to_string());
213 assert_eq!(cache.get(&key), Some(&"rendered content".to_string()));
214 assert_eq!(cache.stats(), (1, 1));
215 assert_eq!(cache.hit_rate(), 50.0);
216
217 cache.clear();
219 assert_eq!(cache.stats(), (0, 0));
220 assert!(cache.get(&key).is_none());
221 }
222}