use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub trait Searchable {
type Query;
fn matches(&self, query: &Self::Query) -> bool;
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SearchQuery {
pub pattern: Option<String>,
pub min_timestamp: Option<DateTime<Utc>>,
pub max_timestamp: Option<DateTime<Utc>>,
pub min_access_count: Option<u64>,
pub max_access_count: Option<u64>,
pub include_expired: bool,
pub category: Option<String>,
}
impl SearchQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_pattern<S: Into<String>>(mut self, pattern: S) -> Self {
self.pattern = Some(pattern.into());
self
}
pub fn with_timestamp_range(
mut self,
min: Option<DateTime<Utc>>,
max: Option<DateTime<Utc>>,
) -> Self {
self.min_timestamp = min;
self.max_timestamp = max;
self
}
pub fn with_access_count_range(mut self, min: Option<u64>, max: Option<u64>) -> Self {
self.min_access_count = min;
self.max_access_count = max;
self
}
pub fn include_expired(mut self, include: bool) -> Self {
self.include_expired = include;
self
}
pub fn with_category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
}
impl<K, V, M> Searchable for crate::CacheEntry<K, V, M>
where
K: Clone + std::hash::Hash + Eq + std::fmt::Display,
V: Clone + std::fmt::Debug,
M: Clone + crate::EntryMetadata,
{
type Query = SearchQuery;
fn matches(&self, query: &Self::Query) -> bool {
let key_str = self.key.to_string();
(query.include_expired || !self.is_expired())
&& query.pattern.as_ref().is_none_or(|p| key_str.contains(p))
&& query.min_timestamp.is_none_or(|min| self.timestamp >= min)
&& query.max_timestamp.is_none_or(|max| self.timestamp <= max)
&& query
.min_access_count
.is_none_or(|min| self.access_count >= min)
&& query
.max_access_count
.is_none_or(|max| self.access_count <= max)
&& query
.category
.as_ref()
.is_none_or(|category| self.metadata.category().is_some_and(|c| c == category))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::CacheEntry;
#[test]
fn test_search_query_builder() {
let query = SearchQuery::new()
.with_pattern("test")
.with_access_count_range(Some(5), Some(10))
.include_expired(true);
assert_eq!(query.pattern, Some("test".to_string()));
assert_eq!(query.min_access_count, Some(5));
assert_eq!(query.max_access_count, Some(10));
assert!(query.include_expired);
}
#[test]
#[allow(clippy::type_complexity)]
fn test_cache_entry_search() {
let mut entry: CacheEntry<String, String, ()> =
CacheEntry::new("test_key".to_string(), "test_value".to_string());
entry.access_count = 7;
let query1 = SearchQuery::new().with_pattern("test");
assert!(entry.matches(&query1));
let query2 = SearchQuery::new().with_pattern("notfound");
assert!(!entry.matches(&query2));
let query3 = SearchQuery::new().with_access_count_range(Some(5), Some(10));
assert!(entry.matches(&query3));
let query4 = SearchQuery::new().with_access_count_range(Some(10), None);
assert!(!entry.matches(&query4));
}
#[test]
fn test_search_query_timestamp_category() {
let now = Utc::now();
let query = SearchQuery::new()
.with_timestamp_range(
Some(now - chrono::Duration::seconds(1)),
Some(now + chrono::Duration::seconds(1)),
)
.with_category("api");
assert!(query.min_timestamp.is_some());
assert_eq!(query.category, Some("api".to_string()));
}
#[test]
fn test_cache_entry_search_branches() {
use crate::entry::BasicMetadata;
let metadata = BasicMetadata {
category: Some("cat".to_string()),
..Default::default()
};
let mut entry = CacheEntry::with_metadata("k".to_string(), "v".to_string(), metadata);
let past = entry.timestamp - chrono::Duration::seconds(10);
let future = entry.timestamp + chrono::Duration::seconds(10);
let q = SearchQuery::new().with_timestamp_range(Some(future), None);
assert!(!entry.matches(&q));
let q2 = SearchQuery::new().with_timestamp_range(Some(past), Some(future));
assert!(entry.matches(&q2));
let q3 = SearchQuery::new().with_category("cat");
assert!(entry.matches(&q3));
let q4 = SearchQuery::new().with_category("other");
assert!(!entry.matches(&q4));
entry.expiry = Some(entry.timestamp - chrono::Duration::seconds(1));
let q5 = SearchQuery::new();
assert!(!entry.matches(&q5));
let q6 = SearchQuery::new().include_expired(true);
assert!(entry.matches(&q6));
}
}