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>,
#[cfg(feature = "json-serialization")]
pub custom_predicates: Option<serde_json::Value>,
#[cfg(not(feature = "json-serialization"))]
pub custom_predicates: 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
}
}
pub trait ExtendedSearch<T> {
fn find_where<F>(&self, predicate: F) -> Vec<T>
where
F: Fn(&T) -> bool;
fn count_where<F>(&self, predicate: F) -> usize
where
F: Fn(&T) -> bool;
fn any<F>(&self, predicate: F) -> bool
where
F: Fn(&T) -> bool;
fn all<F>(&self, predicate: F) -> bool
where
F: Fn(&T) -> bool;
}
#[derive(Debug, Clone)]
pub struct SearchResult<T> {
pub item: T,
pub score: f64,
pub match_details: Vec<String>,
}
impl<T> SearchResult<T> {
pub fn new(item: T, score: f64) -> Self {
Self {
item,
score,
match_details: Vec::new(),
}
}
pub fn with_detail<S: Into<String>>(mut self, detail: S) -> Self {
self.match_details.push(detail.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 {
if !query.include_expired && self.is_expired() {
return false;
}
if let Some(ref pattern) = query.pattern {
let key_str = self.key.to_string();
if !key_str.contains(pattern) {
return false;
}
}
if let Some(min_ts) = query.min_timestamp {
if self.timestamp < min_ts {
return false;
}
}
if let Some(max_ts) = query.max_timestamp {
if self.timestamp > max_ts {
return false;
}
}
if let Some(min_count) = query.min_access_count {
if self.access_count < min_count {
return false;
}
}
if let Some(max_count) = query.max_access_count {
if self.access_count > max_count {
return false;
}
}
if let Some(ref category) = query.category {
if let Some(entry_category) = self.metadata.category() {
if entry_category != category {
return false;
}
} else {
return false;
}
}
true
}
}
#[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));
}
}