main
  1use crate::config::{FilterValue, Rule};
  2use crate::github::Notification;
  3use chrono::Utc;
  4use wildmatch::WildMatch;
  5
  6pub struct RuleMatcher;
  7
  8impl RuleMatcher {
  9    /// Check if a notification matches a rule's filters
 10    pub fn matches(notification: &Notification, rule: &Rule) -> bool {
 11        for (field, value) in &rule.filters {
 12            let matches = match field.as_str() {
 13                "repository" => Self::match_repository(notification, value),
 14                "reason" => Self::match_string(&notification.reason, value),
 15                "subject_type" => Self::match_string(&notification.subject.subject_type, value),
 16                "subject_title" => Self::match_subject_title(notification, value),
 17                "unread" => Self::match_bool(notification.unread, value),
 18                "age_days" => Self::match_age_days(notification, value),
 19                "updated_before" => Self::match_updated_before(notification, value),
 20                "updated_after" => Self::match_updated_after(notification, value),
 21                _ => continue, // Unknown filter, skip
 22            };
 23
 24            if !matches {
 25                return false;
 26            }
 27        }
 28
 29        true
 30    }
 31
 32    /// Find the first matching rule for a notification
 33    pub fn find_matching_rule<'a>(
 34        notification: &Notification,
 35        rules: &'a [&Rule],
 36    ) -> Option<(usize, &'a Rule)> {
 37        for (i, rule) in rules.iter().enumerate() {
 38            if Self::matches(notification, rule) {
 39                return Some((i, rule));
 40            }
 41        }
 42        None
 43    }
 44
 45    fn match_repository(notification: &Notification, value: &FilterValue) -> bool {
 46        if let FilterValue::String(pattern) = value {
 47            let matcher = WildMatch::new(pattern);
 48            matcher.matches(&notification.repository.full_name)
 49        } else {
 50            false
 51        }
 52    }
 53
 54    fn match_subject_title(notification: &Notification, value: &FilterValue) -> bool {
 55        if let FilterValue::String(pattern) = value {
 56            let matcher = WildMatch::new_case_insensitive(pattern);
 57            matcher.matches(&notification.subject.title)
 58        } else {
 59            false
 60        }
 61    }
 62
 63    fn match_string(field_value: &str, filter_value: &FilterValue) -> bool {
 64        if let FilterValue::String(expected) = filter_value {
 65            field_value == expected
 66        } else {
 67            false
 68        }
 69    }
 70
 71    fn match_bool(field_value: bool, filter_value: &FilterValue) -> bool {
 72        if let FilterValue::Bool(expected) = filter_value {
 73            field_value == *expected
 74        } else {
 75            false
 76        }
 77    }
 78
 79    fn match_age_days(notification: &Notification, value: &FilterValue) -> bool {
 80        if let FilterValue::Number(min_days) = value {
 81            let now = Utc::now();
 82            let age = now.signed_duration_since(notification.updated_at);
 83            age.num_days() >= *min_days
 84        } else {
 85            false
 86        }
 87    }
 88
 89    fn match_updated_before(notification: &Notification, value: &FilterValue) -> bool {
 90        if let FilterValue::String(date_str) = value {
 91            if let Ok(cutoff) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
 92                let cutoff = cutoff.and_hms_opt(0, 0, 0).unwrap().and_utc();
 93                notification.updated_at < cutoff
 94            } else {
 95                false
 96            }
 97        } else {
 98            false
 99        }
100    }
101
102    fn match_updated_after(notification: &Notification, value: &FilterValue) -> bool {
103        if let FilterValue::String(date_str) = value {
104            if let Ok(cutoff) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
105                let cutoff = cutoff.and_hms_opt(0, 0, 0).unwrap().and_utc();
106                notification.updated_at > cutoff
107            } else {
108                false
109            }
110        } else {
111            false
112        }
113    }
114}