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