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 #[cfg(feature = "json-serialization")]
34 pub custom_predicates: Option<serde_json::Value>,
35 #[cfg(not(feature = "json-serialization"))]
37 pub custom_predicates: Option<String>,
38}
39
40impl SearchQuery {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn with_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
48 self.pattern = Some(pattern.into());
49 self
50 }
51
52 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 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 pub fn include_expired(mut self, include: bool) -> Self {
72 self.include_expired = include;
73 self
74 }
75
76 pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
78 self.category = Some(category.into());
79 self
80 }
81}
82
83pub trait ExtendedSearch<T> {
85 fn find_where<F>(&self, predicate: F) -> Vec<T>
87 where
88 F: Fn(&T) -> bool;
89
90 fn count_where<F>(&self, predicate: F) -> usize
92 where
93 F: Fn(&T) -> bool;
94
95 fn any<F>(&self, predicate: F) -> bool
97 where
98 F: Fn(&T) -> bool;
99
100 fn all<F>(&self, predicate: F) -> bool
102 where
103 F: Fn(&T) -> bool;
104}
105
106#[derive(Debug, Clone)]
108pub struct SearchResult<T> {
109 pub item: T,
111 pub score: f64,
113 pub match_details: Vec<String>,
115}
116
117impl<T> SearchResult<T> {
118 pub fn new(item: T, score: f64) -> Self {
120 Self {
121 item,
122 score,
123 match_details: Vec::new(),
124 }
125 }
126
127 pub fn with_detail<S: Into<String>>(mut self, detail: S) -> Self {
129 self.match_details.push(detail.into());
130 self
131 }
132}
133
134impl<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 if !query.include_expired && self.is_expired() {
146 return false;
147 }
148
149 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 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 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 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 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 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}