threatflux_cache/
search.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6pub trait Searchable {
8 type Query;
10
11 fn matches(&self, query: &Self::Query) -> bool;
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct SearchQuery {
18 pub pattern: Option<String>,
20 pub min_timestamp: Option<DateTime<Utc>>,
22 pub max_timestamp: Option<DateTime<Utc>>,
24 pub min_access_count: Option<u64>,
26 pub max_access_count: Option<u64>,
28 pub include_expired: bool,
30 pub category: Option<String>,
32}
33
34impl SearchQuery {
35 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn with_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
42 self.pattern = Some(pattern.into());
43 self
44 }
45
46 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 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 pub fn include_expired(mut self, include: bool) -> Self {
66 self.include_expired = include;
67 self
68 }
69
70 pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
72 self.category = Some(category.into());
73 self
74 }
75}
76
77impl<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 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 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 let q = SearchQuery::new().with_timestamp_range(Some(future), None);
172 assert!(!entry.matches(&q));
173
174 let q2 = SearchQuery::new().with_timestamp_range(Some(past), Some(future));
176 assert!(entry.matches(&q2));
177
178 let q3 = SearchQuery::new().with_category("cat");
180 assert!(entry.matches(&q3));
181
182 let q4 = SearchQuery::new().with_category("other");
184 assert!(!entry.matches(&q4));
185
186 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}