Skip to main content

prax_query/data_cache/
key.rs

1//! Cache key generation and patterns.
2
3use std::fmt::{self, Display, Write};
4use std::hash::{Hash, Hasher};
5
6/// A cache key that uniquely identifies a cached value.
7///
8/// Keys are structured as `prefix:namespace:identifier` to enable
9/// pattern-based invalidation.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct CacheKey {
12    /// The key prefix (usually the app name or "prax").
13    prefix: String,
14    /// The namespace (usually entity name like "User", "Post").
15    namespace: String,
16    /// The unique identifier within the namespace.
17    identifier: String,
18    /// Optional tenant ID for multi-tenant apps.
19    tenant: Option<String>,
20}
21
22impl CacheKey {
23    /// Create a new cache key.
24    pub fn new(namespace: impl Into<String>, identifier: impl Into<String>) -> Self {
25        Self {
26            prefix: "prax".to_string(),
27            namespace: namespace.into(),
28            identifier: identifier.into(),
29            tenant: None,
30        }
31    }
32
33    /// Create a cache key with a custom prefix.
34    pub fn with_prefix(
35        prefix: impl Into<String>,
36        namespace: impl Into<String>,
37        identifier: impl Into<String>,
38    ) -> Self {
39        Self {
40            prefix: prefix.into(),
41            namespace: namespace.into(),
42            identifier: identifier.into(),
43            tenant: None,
44        }
45    }
46
47    /// Create a key for a specific entity record.
48    pub fn entity_record<I: Display>(entity: &str, id: I) -> Self {
49        Self::new(entity, format!("id:{}", id))
50    }
51
52    /// Create a key for a query result.
53    pub fn query(entity: &str, query_hash: u64) -> Self {
54        Self::new(entity, format!("query:{:x}", query_hash))
55    }
56
57    /// Create a key for a find-unique query.
58    pub fn find_unique<I: Display>(entity: &str, field: &str, value: I) -> Self {
59        Self::new(entity, format!("unique:{}:{}", field, value))
60    }
61
62    /// Create a key for a find-many query with filters.
63    pub fn find_many(entity: &str, filter_hash: u64) -> Self {
64        Self::new(entity, format!("many:{:x}", filter_hash))
65    }
66
67    /// Create a key for an aggregation.
68    pub fn aggregate(entity: &str, agg_hash: u64) -> Self {
69        Self::new(entity, format!("agg:{:x}", agg_hash))
70    }
71
72    /// Create a key for a relation.
73    pub fn relation<I: Display>(from_entity: &str, from_id: I, relation: &str) -> Self {
74        Self::new(from_entity, format!("rel:{}:{}:{}", from_id, relation, ""))
75    }
76
77    /// Set the tenant for multi-tenant apps.
78    pub fn with_tenant(mut self, tenant: impl Into<String>) -> Self {
79        self.tenant = Some(tenant.into());
80        self
81    }
82
83    /// Get the full key string.
84    pub fn as_str(&self) -> String {
85        let mut key = String::with_capacity(64);
86        key.push_str(&self.prefix);
87        key.push(':');
88
89        if let Some(ref tenant) = self.tenant {
90            key.push_str(tenant);
91            key.push(':');
92        }
93
94        key.push_str(&self.namespace);
95        key.push(':');
96        key.push_str(&self.identifier);
97        key
98    }
99
100    /// Get the namespace.
101    pub fn namespace(&self) -> &str {
102        &self.namespace
103    }
104
105    /// Get the identifier.
106    pub fn identifier(&self) -> &str {
107        &self.identifier
108    }
109
110    /// Get the prefix.
111    pub fn prefix(&self) -> &str {
112        &self.prefix
113    }
114
115    /// Get the tenant if set.
116    pub fn tenant(&self) -> Option<&str> {
117        self.tenant.as_deref()
118    }
119}
120
121impl Display for CacheKey {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "{}", self.as_str())
124    }
125}
126
127impl Hash for CacheKey {
128    fn hash<H: Hasher>(&self, state: &mut H) {
129        self.prefix.hash(state);
130        self.namespace.hash(state);
131        self.identifier.hash(state);
132        self.tenant.hash(state);
133    }
134}
135
136impl From<&str> for CacheKey {
137    fn from(s: &str) -> Self {
138        // Parse "prefix:namespace:identifier" or "namespace:identifier"
139        let parts: Vec<&str> = s.split(':').collect();
140        match parts.len() {
141            2 => Self::new(parts[0], parts[1]),
142            3 => Self::with_prefix(parts[0], parts[1], parts[2]),
143            _ => Self::new("default", s),
144        }
145    }
146}
147
148impl From<String> for CacheKey {
149    fn from(s: String) -> Self {
150        Self::from(s.as_str())
151    }
152}
153
154/// A builder for constructing complex cache keys.
155#[derive(Debug, Default)]
156pub struct CacheKeyBuilder {
157    prefix: Option<String>,
158    namespace: Option<String>,
159    tenant: Option<String>,
160    parts: Vec<String>,
161}
162
163impl CacheKeyBuilder {
164    /// Create a new builder.
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Set the prefix.
170    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
171        self.prefix = Some(prefix.into());
172        self
173    }
174
175    /// Set the namespace (entity).
176    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
177        self.namespace = Some(namespace.into());
178        self
179    }
180
181    /// Set the tenant.
182    pub fn tenant(mut self, tenant: impl Into<String>) -> Self {
183        self.tenant = Some(tenant.into());
184        self
185    }
186
187    /// Add a key part.
188    pub fn part(mut self, part: impl Into<String>) -> Self {
189        self.parts.push(part.into());
190        self
191    }
192
193    /// Add a field-value pair.
194    pub fn field<V: Display>(mut self, name: &str, value: V) -> Self {
195        self.parts.push(format!("{}:{}", name, value));
196        self
197    }
198
199    /// Add an ID.
200    pub fn id<I: Display>(mut self, id: I) -> Self {
201        self.parts.push(format!("id:{}", id));
202        self
203    }
204
205    /// Add a hash.
206    pub fn hash(mut self, hash: u64) -> Self {
207        self.parts.push(format!("{:x}", hash));
208        self
209    }
210
211    /// Build the cache key.
212    pub fn build(self) -> CacheKey {
213        let namespace = self.namespace.unwrap_or_else(|| "default".to_string());
214        let identifier = if self.parts.is_empty() {
215            "default".to_string()
216        } else {
217            self.parts.join(":")
218        };
219
220        let mut key = if let Some(prefix) = self.prefix {
221            CacheKey::with_prefix(prefix, namespace, identifier)
222        } else {
223            CacheKey::new(namespace, identifier)
224        };
225
226        if let Some(tenant) = self.tenant {
227            key = key.with_tenant(tenant);
228        }
229
230        key
231    }
232}
233
234/// A pattern for matching cache keys.
235///
236/// Supports glob-style patterns with `*` wildcards.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub struct KeyPattern {
239    pattern: String,
240}
241
242impl KeyPattern {
243    /// Create a new pattern.
244    pub fn new(pattern: impl Into<String>) -> Self {
245        Self {
246            pattern: pattern.into(),
247        }
248    }
249
250    /// Create a pattern matching all keys for an entity.
251    pub fn entity(entity: &str) -> Self {
252        Self::new(format!("prax:{}:*", entity))
253    }
254
255    /// Create a pattern matching a specific record (with relations).
256    pub fn record<I: Display>(entity: &str, id: I) -> Self {
257        Self::new(format!("prax:{}:*{}*", entity, id))
258    }
259
260    /// Create a pattern for a tenant's data.
261    pub fn tenant(tenant: &str) -> Self {
262        Self::new(format!("prax:{}:*", tenant))
263    }
264
265    /// Create a pattern matching all keys.
266    pub fn all() -> Self {
267        Self::new("prax:*")
268    }
269
270    /// Create a pattern with a custom prefix.
271    pub fn with_prefix(prefix: &str, pattern: &str) -> Self {
272        Self::new(format!("{}:{}", prefix, pattern))
273    }
274
275    /// Get the pattern string.
276    pub fn as_str(&self) -> &str {
277        &self.pattern
278    }
279
280    /// Check if a key matches this pattern.
281    pub fn matches(&self, key: &CacheKey) -> bool {
282        self.matches_str(&key.as_str())
283    }
284
285    /// Check if a string matches this pattern.
286    pub fn matches_str(&self, key: &str) -> bool {
287        glob_match(&self.pattern, key)
288    }
289
290    /// Convert to a Redis-compatible pattern.
291    pub fn to_redis_pattern(&self) -> String {
292        self.pattern.clone()
293    }
294}
295
296impl Display for KeyPattern {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        write!(f, "{}", self.pattern)
299    }
300}
301
302/// Simple glob matching with `*` wildcards.
303fn glob_match(pattern: &str, text: &str) -> bool {
304    let mut pattern_chars = pattern.chars().peekable();
305    let mut text_chars = text.chars().peekable();
306
307    while let Some(p) = pattern_chars.next() {
308        match p {
309            '*' => {
310                // Match any number of characters
311                if pattern_chars.peek().is_none() {
312                    return true; // Trailing * matches everything
313                }
314
315                // Try matching from current position
316                let remaining_pattern: String = pattern_chars.collect();
317                let remaining_text: String = text_chars.collect();
318
319                for i in 0..=remaining_text.len() {
320                    if glob_match(&remaining_pattern, &remaining_text[i..]) {
321                        return true;
322                    }
323                }
324                return false;
325            }
326            '?' => {
327                // Match exactly one character
328                if text_chars.next().is_none() {
329                    return false;
330                }
331            }
332            c => {
333                // Match literal character
334                match text_chars.next() {
335                    Some(t) if t == c => {}
336                    _ => return false,
337                }
338            }
339        }
340    }
341
342    text_chars.next().is_none()
343}
344
345/// Helper to compute a hash for cache keys.
346pub fn compute_hash<T: Hash>(value: &T) -> u64 {
347    use std::collections::hash_map::DefaultHasher;
348    let mut hasher = DefaultHasher::new();
349    value.hash(&mut hasher);
350    hasher.finish()
351}
352
353/// Helper to compute a hash from multiple values.
354pub fn compute_hash_many<T: Hash>(values: &[T]) -> u64 {
355    use std::collections::hash_map::DefaultHasher;
356    let mut hasher = DefaultHasher::new();
357    for value in values {
358        value.hash(&mut hasher);
359    }
360    hasher.finish()
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_cache_key_creation() {
369        let key = CacheKey::new("User", "id:123");
370        assert_eq!(key.as_str(), "prax:User:id:123");
371    }
372
373    #[test]
374    fn test_cache_key_with_tenant() {
375        let key = CacheKey::new("User", "id:123").with_tenant("tenant-1");
376        assert_eq!(key.as_str(), "prax:tenant-1:User:id:123");
377    }
378
379    #[test]
380    fn test_entity_record_key() {
381        let key = CacheKey::entity_record("User", 42);
382        assert_eq!(key.as_str(), "prax:User:id:42");
383    }
384
385    #[test]
386    fn test_find_unique_key() {
387        let key = CacheKey::find_unique("User", "email", "test@example.com");
388        assert_eq!(key.as_str(), "prax:User:unique:email:test@example.com");
389    }
390
391    #[test]
392    fn test_key_builder() {
393        let key = CacheKeyBuilder::new()
394            .namespace("User")
395            .field("status", "active")
396            .id(123)
397            .build();
398
399        assert!(key.as_str().contains("User"));
400        assert!(key.as_str().contains("status:active"));
401    }
402
403    #[test]
404    fn test_key_pattern_entity() {
405        let pattern = KeyPattern::entity("User");
406        assert_eq!(pattern.as_str(), "prax:User:*");
407
408        let key1 = CacheKey::entity_record("User", 1);
409        let key2 = CacheKey::entity_record("Post", 1);
410
411        assert!(pattern.matches(&key1));
412        assert!(!pattern.matches(&key2));
413    }
414
415    #[test]
416    fn test_glob_matching() {
417        assert!(glob_match("*", "anything"));
418        assert!(glob_match("prax:*", "prax:User:123"));
419        assert!(glob_match("prax:User:*", "prax:User:id:123"));
420        assert!(!glob_match("prax:Post:*", "prax:User:id:123"));
421        assert!(glob_match("*:User:*", "prax:User:id:123"));
422    }
423
424    #[test]
425    fn test_compute_hash() {
426        let hash1 = compute_hash(&"test");
427        let hash2 = compute_hash(&"test");
428        let hash3 = compute_hash(&"other");
429
430        assert_eq!(hash1, hash2);
431        assert_ne!(hash1, hash3);
432    }
433}