rs_query/
key.rs

1//! Query key system for cache lookup and hierarchical invalidation
2
3use std::hash::{Hash, Hasher};
4
5/// A hierarchical query key for cache lookup and invalidation.
6///
7/// Keys can have static and dynamic segments for flexible matching:
8///
9/// ```rust,ignore
10/// // Static key
11/// QueryKey::new("users")
12///
13/// // With parameters
14/// QueryKey::new("users").with("id", user_id)
15///
16/// // Hierarchical
17/// QueryKey::new("users").segment("posts").with("id", post_id)
18/// ```
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct QueryKey {
21    segments: Vec<KeySegment>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25enum KeySegment {
26    Static(String),
27    Dynamic(String, String), // (name, value)
28}
29
30impl QueryKey {
31    /// Create a new query key with a root segment
32    pub fn new(root: impl Into<String>) -> Self {
33        Self {
34            segments: vec![KeySegment::Static(root.into())],
35        }
36    }
37
38    /// Add a static segment
39    pub fn segment(mut self, segment: impl Into<String>) -> Self {
40        self.segments.push(KeySegment::Static(segment.into()));
41        self
42    }
43
44    /// Add a dynamic segment (parameterized)
45    pub fn with(mut self, name: impl Into<String>, value: impl ToString) -> Self {
46        self.segments
47            .push(KeySegment::Dynamic(name.into(), value.to_string()));
48        self
49    }
50
51    /// Check if this key matches another for invalidation.
52    /// Returns true if `pattern` is a prefix of or equal to `self`.
53    pub fn matches(&self, pattern: &QueryKey) -> bool {
54        if pattern.segments.len() > self.segments.len() {
55            return false;
56        }
57        self.segments
58            .iter()
59            .zip(pattern.segments.iter())
60            .all(|(a, b)| a == b)
61    }
62
63    /// Get cache key string for HashMap lookup
64    pub fn cache_key(&self) -> String {
65        self.segments
66            .iter()
67            .map(|s| match s {
68                KeySegment::Static(v) => v.clone(),
69                KeySegment::Dynamic(k, v) => format!("{}={}", k, v),
70            })
71            .collect::<Vec<_>>()
72            .join("::")
73    }
74}
75
76impl Hash for QueryKey {
77    fn hash<H: Hasher>(&self, state: &mut H) {
78        self.cache_key().hash(state);
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_simple_key() {
88        let key = QueryKey::new("users");
89        assert_eq!(key.cache_key(), "users");
90    }
91
92    #[test]
93    fn test_key_with_params() {
94        let key = QueryKey::new("users").with("id", 123);
95        assert_eq!(key.cache_key(), "users::id=123");
96    }
97
98    #[test]
99    fn test_hierarchical_key() {
100        let key = QueryKey::new("users").segment("posts").with("id", 456);
101        assert_eq!(key.cache_key(), "users::posts::id=456");
102    }
103
104    #[test]
105    fn test_key_matching() {
106        let full = QueryKey::new("users").with("id", 123);
107        let pattern = QueryKey::new("users");
108
109        assert!(full.matches(&pattern));
110        assert!(!pattern.matches(&full));
111    }
112}