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(¬ification.reason, value),
15 "subject_type" => Self::match_string(¬ification.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(¬ification.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(¬ification.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}