use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MetadataFilter {
Eq {
field: String,
value: String,
},
Ne {
field: String,
value: String,
},
Contains {
field: String,
substring: String,
},
Exists {
field: String,
},
NotExists {
field: String,
},
And(Vec<MetadataFilter>),
Or(Vec<MetadataFilter>),
}
impl MetadataFilter {
#[must_use]
pub fn matches(&self, metadata: &HashMap<String, String>) -> bool {
match self {
Self::Eq { field, value } => metadata.get(field).is_some_and(|v| v == value),
Self::Ne { field, value } => metadata.get(field).is_none_or(|v| v != value),
Self::Contains { field, substring } => {
metadata.get(field).is_some_and(|v| v.contains(substring))
}
Self::Exists { field } => metadata.contains_key(field),
Self::NotExists { field } => !metadata.contains_key(field),
Self::And(filters) => filters.iter().all(|f| f.matches(metadata)),
Self::Or(filters) => {
if filters.is_empty() {
true
} else {
filters.iter().any(|f| f.matches(metadata))
}
}
}
}
#[must_use]
pub fn eq(field: impl Into<String>, value: impl Into<String>) -> Self {
Self::Eq {
field: field.into(),
value: value.into(),
}
}
#[must_use]
pub fn ne(field: impl Into<String>, value: impl Into<String>) -> Self {
Self::Ne {
field: field.into(),
value: value.into(),
}
}
#[must_use]
pub fn contains(field: impl Into<String>, substring: impl Into<String>) -> Self {
Self::Contains {
field: field.into(),
substring: substring.into(),
}
}
#[must_use]
pub fn exists(field: impl Into<String>) -> Self {
Self::Exists {
field: field.into(),
}
}
#[must_use]
pub fn not_exists(field: impl Into<String>) -> Self {
Self::NotExists {
field: field.into(),
}
}
#[must_use]
pub fn and(filters: Vec<MetadataFilter>) -> Self {
Self::And(filters)
}
#[must_use]
pub fn or(filters: Vec<MetadataFilter>) -> Self {
Self::Or(filters)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_metadata(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
#[test]
fn test_eq_filter() {
let filter = MetadataFilter::eq("category", "science");
let matching = make_metadata(&[("category", "science")]);
let non_matching = make_metadata(&[("category", "technology")]);
let missing = make_metadata(&[("other", "value")]);
assert!(filter.matches(&matching));
assert!(!filter.matches(&non_matching));
assert!(!filter.matches(&missing));
}
#[test]
fn test_ne_filter() {
let filter = MetadataFilter::ne("status", "draft");
let matching_different = make_metadata(&[("status", "published")]);
let matching_missing = make_metadata(&[("other", "value")]);
let non_matching = make_metadata(&[("status", "draft")]);
assert!(filter.matches(&matching_different));
assert!(filter.matches(&matching_missing));
assert!(!filter.matches(&non_matching));
}
#[test]
fn test_contains_filter() {
let filter = MetadataFilter::contains("title", "Rust");
let matching = make_metadata(&[("title", "Learning Rust Programming")]);
let non_matching = make_metadata(&[("title", "Python Guide")]);
let missing = make_metadata(&[("other", "value")]);
assert!(filter.matches(&matching));
assert!(!filter.matches(&non_matching));
assert!(!filter.matches(&missing));
}
#[test]
fn test_exists_filter() {
let filter = MetadataFilter::exists("author");
let matching = make_metadata(&[("author", "John Doe")]);
let non_matching = make_metadata(&[("title", "Book")]);
assert!(filter.matches(&matching));
assert!(!filter.matches(&non_matching));
}
#[test]
fn test_not_exists_filter() {
let filter = MetadataFilter::not_exists("deprecated");
let matching = make_metadata(&[("status", "active")]);
let non_matching = make_metadata(&[("deprecated", "true")]);
assert!(filter.matches(&matching));
assert!(!filter.matches(&non_matching));
}
#[test]
fn test_and_filter() {
let filter = MetadataFilter::and(vec![
MetadataFilter::eq("status", "published"),
MetadataFilter::eq("category", "science"),
]);
let matching = make_metadata(&[("status", "published"), ("category", "science")]);
let partial1 = make_metadata(&[("status", "published"), ("category", "tech")]);
let partial2 = make_metadata(&[("status", "draft"), ("category", "science")]);
assert!(filter.matches(&matching));
assert!(!filter.matches(&partial1));
assert!(!filter.matches(&partial2));
}
#[test]
fn test_and_filter_empty() {
let filter = MetadataFilter::and(vec![]);
let metadata = make_metadata(&[("any", "value")]);
assert!(filter.matches(&metadata));
}
#[test]
fn test_or_filter() {
let filter = MetadataFilter::or(vec![
MetadataFilter::eq("category", "science"),
MetadataFilter::eq("category", "technology"),
]);
let matching1 = make_metadata(&[("category", "science")]);
let matching2 = make_metadata(&[("category", "technology")]);
let non_matching = make_metadata(&[("category", "art")]);
assert!(filter.matches(&matching1));
assert!(filter.matches(&matching2));
assert!(!filter.matches(&non_matching));
}
#[test]
fn test_or_filter_empty() {
let filter = MetadataFilter::or(vec![]);
let metadata = make_metadata(&[("any", "value")]);
assert!(filter.matches(&metadata));
}
#[test]
fn test_complex_nested_filter() {
let filter = MetadataFilter::and(vec![
MetadataFilter::eq("status", "published"),
MetadataFilter::or(vec![
MetadataFilter::eq("category", "science"),
MetadataFilter::eq("category", "tech"),
]),
]);
let matching1 = make_metadata(&[("status", "published"), ("category", "science")]);
let matching2 = make_metadata(&[("status", "published"), ("category", "tech")]);
let non_matching1 = make_metadata(&[("status", "draft"), ("category", "science")]);
let non_matching2 = make_metadata(&[("status", "published"), ("category", "art")]);
assert!(filter.matches(&matching1));
assert!(filter.matches(&matching2));
assert!(!filter.matches(&non_matching1));
assert!(!filter.matches(&non_matching2));
}
#[test]
fn test_filter_equality() {
let filter1 = MetadataFilter::eq("field", "value");
let filter2 = MetadataFilter::eq("field", "value");
let filter3 = MetadataFilter::eq("field", "other");
assert_eq!(filter1, filter2);
assert_ne!(filter1, filter3);
}
#[test]
fn test_filter_clone() {
let original = MetadataFilter::and(vec![
MetadataFilter::eq("a", "b"),
MetadataFilter::or(vec![MetadataFilter::exists("c")]),
]);
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_filter_debug() {
let filter = MetadataFilter::eq("field", "value");
let debug_str = format!("{filter:?}");
assert!(debug_str.contains("Eq"));
assert!(debug_str.contains("field"));
assert!(debug_str.contains("value"));
}
}