Skip to main content

threatflux_cache/
search.rs

1//! Search and query functionality for cache entries
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Trait for searchable cache entries
7pub trait Searchable {
8    /// Query type for searching
9    type Query;
10
11    /// Check if this entry matches the query
12    fn matches(&self, query: &Self::Query) -> bool;
13}
14
15/// Basic search query for cache entries
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct SearchQuery {
18    /// Pattern to match in string representation
19    pub pattern: Option<String>,
20    /// Minimum timestamp
21    pub min_timestamp: Option<DateTime<Utc>>,
22    /// Maximum timestamp
23    pub max_timestamp: Option<DateTime<Utc>>,
24    /// Minimum access count
25    pub min_access_count: Option<u64>,
26    /// Maximum access count
27    pub max_access_count: Option<u64>,
28    /// Include expired entries
29    pub include_expired: bool,
30    /// Category filter
31    pub category: Option<String>,
32}
33
34impl SearchQuery {
35    /// Create a new empty search query
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Set pattern to search for
41    pub fn with_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
42        self.pattern = Some(pattern.into());
43        self
44    }
45
46    /// Set timestamp range
47    pub fn with_timestamp_range(
48        mut self,
49        min: Option<DateTime<Utc>>,
50        max: Option<DateTime<Utc>>,
51    ) -> Self {
52        self.min_timestamp = min;
53        self.max_timestamp = max;
54        self
55    }
56
57    /// Set access count range
58    pub fn with_access_count_range(mut self, min: Option<u64>, max: Option<u64>) -> Self {
59        self.min_access_count = min;
60        self.max_access_count = max;
61        self
62    }
63
64    /// Set whether to include expired entries
65    pub fn include_expired(mut self, include: bool) -> Self {
66        self.include_expired = include;
67        self
68    }
69
70    /// Set category filter
71    pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
72        self.category = Some(category.into());
73        self
74    }
75}
76
77/// Implement Searchable for common types
78impl<K, V, M> Searchable for crate::CacheEntry<K, V, M>
79where
80    K: Clone + std::hash::Hash + Eq + std::fmt::Display,
81    V: Clone + std::fmt::Debug,
82    M: Clone + crate::EntryMetadata,
83{
84    type Query = SearchQuery;
85
86    fn matches(&self, query: &Self::Query) -> bool {
87        let key_str = self.key.to_string();
88        (query.include_expired || !self.is_expired())
89            && query.pattern.as_ref().is_none_or(|p| key_str.contains(p))
90            && query.min_timestamp.is_none_or(|min| self.timestamp >= min)
91            && query.max_timestamp.is_none_or(|max| self.timestamp <= max)
92            && query
93                .min_access_count
94                .is_none_or(|min| self.access_count >= min)
95            && query
96                .max_access_count
97                .is_none_or(|max| self.access_count <= max)
98            && query
99                .category
100                .as_ref()
101                .is_none_or(|category| self.metadata.category().is_some_and(|c| c == category))
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::CacheEntry;
109
110    #[test]
111    fn test_search_query_builder() {
112        let query = SearchQuery::new()
113            .with_pattern("test")
114            .with_access_count_range(Some(5), Some(10))
115            .include_expired(true);
116
117        assert_eq!(query.pattern, Some("test".to_string()));
118        assert_eq!(query.min_access_count, Some(5));
119        assert_eq!(query.max_access_count, Some(10));
120        assert!(query.include_expired);
121    }
122
123    #[test]
124    #[allow(clippy::type_complexity)]
125    fn test_cache_entry_search() {
126        let mut entry: CacheEntry<String, String, ()> =
127            CacheEntry::new("test_key".to_string(), "test_value".to_string());
128        entry.access_count = 7;
129
130        // Test pattern matching
131        let query1 = SearchQuery::new().with_pattern("test");
132        assert!(entry.matches(&query1));
133
134        let query2 = SearchQuery::new().with_pattern("notfound");
135        assert!(!entry.matches(&query2));
136
137        // Test access count range
138        let query3 = SearchQuery::new().with_access_count_range(Some(5), Some(10));
139        assert!(entry.matches(&query3));
140
141        let query4 = SearchQuery::new().with_access_count_range(Some(10), None);
142        assert!(!entry.matches(&query4));
143    }
144
145    #[test]
146    fn test_search_query_timestamp_category() {
147        let now = Utc::now();
148        let query = SearchQuery::new()
149            .with_timestamp_range(
150                Some(now - chrono::Duration::seconds(1)),
151                Some(now + chrono::Duration::seconds(1)),
152            )
153            .with_category("api");
154        assert!(query.min_timestamp.is_some());
155        assert_eq!(query.category, Some("api".to_string()));
156    }
157
158    #[test]
159    fn test_cache_entry_search_branches() {
160        use crate::entry::BasicMetadata;
161        let metadata = BasicMetadata {
162            category: Some("cat".to_string()),
163            ..Default::default()
164        };
165        let mut entry = CacheEntry::with_metadata("k".to_string(), "v".to_string(), metadata);
166
167        let past = entry.timestamp - chrono::Duration::seconds(10);
168        let future = entry.timestamp + chrono::Duration::seconds(10);
169
170        // timestamp range not matching
171        let q = SearchQuery::new().with_timestamp_range(Some(future), None);
172        assert!(!entry.matches(&q));
173
174        // timestamp range matching
175        let q2 = SearchQuery::new().with_timestamp_range(Some(past), Some(future));
176        assert!(entry.matches(&q2));
177
178        // category matching
179        let q3 = SearchQuery::new().with_category("cat");
180        assert!(entry.matches(&q3));
181
182        // category mismatch
183        let q4 = SearchQuery::new().with_category("other");
184        assert!(!entry.matches(&q4));
185
186        // expired handling
187        entry.expiry = Some(entry.timestamp - chrono::Duration::seconds(1));
188        let q5 = SearchQuery::new();
189        assert!(!entry.matches(&q5));
190        let q6 = SearchQuery::new().include_expired(true);
191        assert!(entry.matches(&q6));
192    }
193}