use super::AttrValue;
use crate::model::Metadata;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilterOp {
Eq,
Ne,
Contains,
ContainsAll,
}
#[derive(Debug, Clone)]
pub struct AttrFilter {
pub attr: String,
pub op: FilterOp,
pub value: AttrValue,
}
impl AttrFilter {
pub fn new(attr: impl Into<String>, op: FilterOp, value: AttrValue) -> Self {
Self {
attr: attr.into(),
op,
value,
}
}
pub fn eq(attr: impl Into<String>, value: AttrValue) -> Self {
Self::new(attr, FilterOp::Eq, value)
}
pub fn ne(attr: impl Into<String>, value: AttrValue) -> Self {
Self::new(attr, FilterOp::Ne, value)
}
pub fn contains(attr: impl Into<String>, value: String) -> Self {
Self::new(attr, FilterOp::Contains, AttrValue::List(vec![value]))
}
pub fn contains_all(attr: impl Into<String>, values: Vec<String>) -> Self {
Self::new(attr, FilterOp::ContainsAll, AttrValue::List(values))
}
pub fn matches(&self, meta: &Metadata) -> bool {
let Some(attr_value) = meta.get_attr(&self.attr) else {
return false;
};
match &self.op {
FilterOp::Eq => self.values_equal(&attr_value, &self.value),
FilterOp::Ne => !self.values_equal(&attr_value, &self.value),
FilterOp::Contains => self.list_contains(&attr_value, &self.value),
FilterOp::ContainsAll => self.list_contains_all(&attr_value, &self.value),
}
}
fn values_equal(&self, a: &AttrValue, b: &AttrValue) -> bool {
match (a, b) {
(AttrValue::Bool(a_val), AttrValue::Bool(b_val)) => a_val == b_val,
(
AttrValue::BoolWithTimestamp { value: a_val, .. },
AttrValue::BoolWithTimestamp { value: b_val, .. },
) => a_val == b_val,
(AttrValue::BoolWithTimestamp { value: a_val, .. }, AttrValue::Bool(b_val)) => {
a_val == b_val
}
(AttrValue::Bool(a_val), AttrValue::BoolWithTimestamp { value: b_val, .. }) => {
a_val == b_val
}
(AttrValue::Enum(a_val), AttrValue::Enum(b_val)) => a_val == b_val,
(AttrValue::List(a_list), AttrValue::List(b_list)) => a_list == b_list,
(AttrValue::Ref(a_ref), AttrValue::Ref(b_ref)) => a_ref == b_ref,
_ => false, }
}
fn list_contains(&self, attr_value: &AttrValue, filter_value: &AttrValue) -> bool {
let AttrValue::List(attr_list) = attr_value else {
return false;
};
let AttrValue::List(filter_list) = filter_value else {
return false;
};
filter_list.iter().any(|v| attr_list.contains(v))
}
fn list_contains_all(&self, attr_value: &AttrValue, filter_value: &AttrValue) -> bool {
let AttrValue::List(attr_list) = attr_value else {
return false;
};
let AttrValue::List(filter_list) = filter_value else {
return false;
};
filter_list.iter().all(|v| attr_list.contains(v))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::TodoStatus;
fn meta_with_status(status: TodoStatus) -> Metadata {
let mut meta = Metadata::new("Test".into());
meta.status = status;
meta
}
fn meta_with_tags(tags: Vec<&str>) -> Metadata {
let mut meta = Metadata::new("Test".into());
meta.tags = tags.into_iter().map(|s| s.to_string()).collect();
meta
}
fn meta_pinned() -> Metadata {
let mut meta = Metadata::new("Test".into());
meta.is_pinned = true;
meta.pinned_at = Some(chrono::Utc::now());
meta
}
#[test]
fn filter_eq_bool() {
let filter = AttrFilter::eq("pinned", AttrValue::Bool(true));
assert!(filter.matches(&meta_pinned()));
assert!(!filter.matches(&Metadata::new("Test".into())));
}
#[test]
fn filter_ne_bool() {
let filter = AttrFilter::ne("pinned", AttrValue::Bool(true));
assert!(!filter.matches(&meta_pinned()));
assert!(filter.matches(&Metadata::new("Test".into())));
}
#[test]
fn filter_eq_status() {
let filter = AttrFilter::eq("status", AttrValue::Enum("Done".into()));
assert!(filter.matches(&meta_with_status(TodoStatus::Done)));
assert!(!filter.matches(&meta_with_status(TodoStatus::Planned)));
assert!(!filter.matches(&meta_with_status(TodoStatus::InProgress)));
}
#[test]
fn filter_ne_status() {
let filter = AttrFilter::ne("status", AttrValue::Enum("Done".into()));
assert!(!filter.matches(&meta_with_status(TodoStatus::Done)));
assert!(filter.matches(&meta_with_status(TodoStatus::Planned)));
assert!(filter.matches(&meta_with_status(TodoStatus::InProgress)));
}
#[test]
fn filter_contains_single_tag() {
let filter = AttrFilter::contains("tags", "rust".into());
assert!(filter.matches(&meta_with_tags(vec!["rust"])));
assert!(filter.matches(&meta_with_tags(vec!["rust", "work"])));
assert!(!filter.matches(&meta_with_tags(vec!["python"])));
assert!(!filter.matches(&meta_with_tags(vec![])));
}
#[test]
fn filter_contains_all_tags() {
let filter = AttrFilter::contains_all("tags", vec!["rust".into(), "work".into()]);
assert!(filter.matches(&meta_with_tags(vec!["rust", "work"])));
assert!(filter.matches(&meta_with_tags(vec!["rust", "work", "urgent"])));
assert!(!filter.matches(&meta_with_tags(vec!["rust"])));
assert!(!filter.matches(&meta_with_tags(vec!["work"])));
assert!(!filter.matches(&meta_with_tags(vec![])));
}
#[test]
fn filter_unknown_attr_returns_false() {
let filter = AttrFilter::eq("unknown", AttrValue::Bool(true));
assert!(!filter.matches(&Metadata::new("Test".into())));
}
#[test]
fn filter_type_mismatch_returns_false() {
let filter = AttrFilter::eq("status", AttrValue::Bool(true));
assert!(!filter.matches(&meta_with_status(TodoStatus::Done)));
}
}