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}
36
37impl RenderCacheKey {
38 #[must_use]
40 pub(crate) fn new(
41 resource_path: String,
42 resource_type: ResourceType,
43 tool: Option<String>,
44 variant_inputs_hash: String,
45 resolved_commit: Option<String>,
46 ) -> Self {
47 Self {
48 resource_path,
49 resource_type,
50 tool,
51 variant_inputs_hash,
52 resolved_commit,
53 }
54 }
55}
56
57#[derive(Debug, Default)]
75pub(crate) struct RenderCache {
76 cache: HashMap<RenderCacheKey, String>,
78 hits: usize,
80 misses: usize,
81}
82
83impl RenderCache {
84 #[must_use]
86 pub(crate) fn new() -> Self {
87 Self {
88 cache: HashMap::new(),
89 hits: 0,
90 misses: 0,
91 }
92 }
93
94 pub(crate) fn get(&mut self, key: &RenderCacheKey) -> Option<&String> {
96 if let Some(content) = self.cache.get(key) {
97 self.hits += 1;
98 Some(content)
99 } else {
100 self.misses += 1;
101 None
102 }
103 }
104
105 pub(crate) fn insert(&mut self, key: RenderCacheKey, content: String) {
107 self.cache.insert(key, content);
108 }
109
110 pub(crate) fn clear(&mut self) {
112 self.cache.clear();
113 self.hits = 0;
114 self.misses = 0;
115 }
116
117 #[must_use]
119 pub(crate) fn stats(&self) -> (usize, usize) {
120 (self.hits, self.misses)
121 }
122
123 #[must_use]
125 pub(crate) fn hit_rate(&self) -> f64 {
126 let total = self.hits + self.misses;
127 if total == 0 {
128 0.0
129 } else {
130 (self.hits as f64 / total as f64) * 100.0
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_render_cache_key_includes_commit() {
141 let key1 = RenderCacheKey::new(
143 "agents/helper.md".to_string(),
144 ResourceType::Agent,
145 Some("claude-code".to_string()),
146 "hash123".to_string(),
147 Some("abc123def".to_string()),
148 );
149
150 let key2 = RenderCacheKey::new(
151 "agents/helper.md".to_string(),
152 ResourceType::Agent,
153 Some("claude-code".to_string()),
154 "hash123".to_string(),
155 Some("def456ghi".to_string()),
156 );
157
158 assert_ne!(key1, key2, "Different commits should have different cache keys");
159
160 let key3 = RenderCacheKey::new(
162 "agents/helper.md".to_string(),
163 ResourceType::Agent,
164 Some("claude-code".to_string()),
165 "hash123".to_string(),
166 Some("abc123def".to_string()),
167 );
168
169 assert_eq!(key1, key3, "Same commits should have identical cache keys");
170
171 let key4 = RenderCacheKey::new(
173 "agents/helper.md".to_string(),
174 ResourceType::Agent,
175 Some("claude-code".to_string()),
176 "hash123".to_string(),
177 None,
178 );
179
180 assert_ne!(key1, key4, "Some(commit) vs None should have different cache keys");
181 }
182
183 #[test]
184 fn test_render_cache_basic_operations() {
185 let mut cache = RenderCache::new();
186 let key = RenderCacheKey::new(
187 "test.md".to_string(),
188 ResourceType::Snippet,
189 Some("claude-code".to_string()),
190 "hash789".to_string(),
191 Some("commit123".to_string()),
192 );
193
194 assert_eq!(cache.stats(), (0, 0));
196 assert_eq!(cache.hit_rate(), 0.0);
197
198 assert!(cache.get(&key).is_none());
200 assert_eq!(cache.stats(), (0, 1));
201
202 cache.insert(key.clone(), "rendered content".to_string());
204 assert_eq!(cache.get(&key), Some(&"rendered content".to_string()));
205 assert_eq!(cache.stats(), (1, 1));
206 assert_eq!(cache.hit_rate(), 50.0);
207
208 cache.clear();
210 assert_eq!(cache.stats(), (0, 0));
211 assert!(cache.get(&key).is_none());
212 }
213}