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    /// Custom predicates as JSON
33    #[cfg(feature = "json-serialization")]
34    pub custom_predicates: Option<serde_json::Value>,
35    /// Custom predicates as string (when JSON feature is disabled)
36    #[cfg(not(feature = "json-serialization"))]
37    pub custom_predicates: Option<String>,
38}
39
40impl SearchQuery {
41    /// Create a new empty search query
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set pattern to search for
47    pub fn with_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
48        self.pattern = Some(pattern.into());
49        self
50    }
51
52    /// Set timestamp range
53    pub fn with_timestamp_range(
54        mut self,
55        min: Option<DateTime<Utc>>,
56        max: Option<DateTime<Utc>>,
57    ) -> Self {
58        self.min_timestamp = min;
59        self.max_timestamp = max;
60        self
61    }
62
63    /// Set access count range
64    pub fn with_access_count_range(mut self, min: Option<u64>, max: Option<u64>) -> Self {
65        self.min_access_count = min;
66        self.max_access_count = max;
67        self
68    }
69
70    /// Set whether to include expired entries
71    pub fn include_expired(mut self, include: bool) -> Self {
72        self.include_expired = include;
73        self
74    }
75
76    /// Set category filter
77    pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
78        self.category = Some(category.into());
79        self
80    }
81}
82
83/// Extended search capabilities
84pub trait ExtendedSearch<T> {
85    /// Find entries matching a predicate
86    fn find_where<F>(&self, predicate: F) -> Vec<T>
87    where
88        F: Fn(&T) -> bool;
89
90    /// Count entries matching a predicate
91    fn count_where<F>(&self, predicate: F) -> usize
92    where
93        F: Fn(&T) -> bool;
94
95    /// Check if any entry matches a predicate
96    fn any<F>(&self, predicate: F) -> bool
97    where
98        F: Fn(&T) -> bool;
99
100    /// Check if all entries match a predicate
101    fn all<F>(&self, predicate: F) -> bool
102    where
103        F: Fn(&T) -> bool;
104}
105
106/// Search result with relevance scoring
107#[derive(Debug, Clone)]
108pub struct SearchResult<T> {
109    /// The matched item
110    pub item: T,
111    /// Relevance score (0.0 to 1.0)
112    pub score: f64,
113    /// Match details
114    pub match_details: Vec<String>,
115}
116
117impl<T> SearchResult<T> {
118    /// Create a new search result
119    pub fn new(item: T, score: f64) -> Self {
120        Self {
121            item,
122            score,
123            match_details: Vec::new(),
124        }
125    }
126
127    /// Add match detail
128    pub fn with_detail<S: Into<String>>(mut self, detail: S) -> Self {
129        self.match_details.push(detail.into());
130        self
131    }
132}
133
134/// Implement Searchable for common types
135impl<K, V, M> Searchable for crate::CacheEntry<K, V, M>
136where
137    K: Clone + std::hash::Hash + Eq + std::fmt::Display,
138    V: Clone + std::fmt::Debug,
139    M: Clone + crate::EntryMetadata,
140{
141    type Query = SearchQuery;
142
143    fn matches(&self, query: &Self::Query) -> bool {
144        // Check expiry
145        if !query.include_expired && self.is_expired() {
146            return false;
147        }
148
149        // Check pattern in key
150        if let Some(ref pattern) = query.pattern {
151            let key_str = self.key.to_string();
152            if !key_str.contains(pattern) {
153                return false;
154            }
155        }
156
157        // Check timestamp range
158        if let Some(min_ts) = query.min_timestamp {
159            if self.timestamp < min_ts {
160                return false;
161            }
162        }
163
164        if let Some(max_ts) = query.max_timestamp {
165            if self.timestamp > max_ts {
166                return false;
167            }
168        }
169
170        // Check access count range
171        if let Some(min_count) = query.min_access_count {
172            if self.access_count < min_count {
173                return false;
174            }
175        }
176
177        if let Some(max_count) = query.max_access_count {
178            if self.access_count > max_count {
179                return false;
180            }
181        }
182
183        // Check category
184        if let Some(ref category) = query.category {
185            if let Some(entry_category) = self.metadata.category() {
186                if entry_category != category {
187                    return false;
188                }
189            } else {
190                return false;
191            }
192        }
193
194        true
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::CacheEntry;
202
203    #[test]
204    fn test_search_query_builder() {
205        let query = SearchQuery::new()
206            .with_pattern("test")
207            .with_access_count_range(Some(5), Some(10))
208            .include_expired(true);
209
210        assert_eq!(query.pattern, Some("test".to_string()));
211        assert_eq!(query.min_access_count, Some(5));
212        assert_eq!(query.max_access_count, Some(10));
213        assert!(query.include_expired);
214    }
215
216    #[test]
217    #[allow(clippy::type_complexity)]
218    fn test_cache_entry_search() {
219        let mut entry: CacheEntry<String, String, ()> =
220            CacheEntry::new("test_key".to_string(), "test_value".to_string());
221        entry.access_count = 7;
222
223        // Test pattern matching
224        let query1 = SearchQuery::new().with_pattern("test");
225        assert!(entry.matches(&query1));
226
227        let query2 = SearchQuery::new().with_pattern("notfound");
228        assert!(!entry.matches(&query2));
229
230        // Test access count range
231        let query3 = SearchQuery::new().with_access_count_range(Some(5), Some(10));
232        assert!(entry.matches(&query3));
233
234        let query4 = SearchQuery::new().with_access_count_range(Some(10), None);
235        assert!(!entry.matches(&query4));
236    }
237}