semantic bufo search find-bufo.com
bufo
at main 193 lines 5.0 kB view raw
1//! composable result filters 2//! 3//! filters are predicates that can be combined to create complex filtering logic. 4 5use regex::Regex; 6 7/// a single search result that can be filtered 8pub trait Filterable { 9 fn name(&self) -> &str; 10} 11 12/// a predicate that can accept or reject items 13pub trait Filter<T: Filterable>: Send + Sync { 14 /// returns true if the item should be kept 15 fn matches(&self, item: &T) -> bool; 16} 17 18/// filters out inappropriate content based on a blocklist 19struct BlocklistFilter { 20 blocklist: Vec<&'static str>, 21} 22 23impl BlocklistFilter { 24 fn inappropriate_bufos() -> Self { 25 Self { 26 blocklist: vec![ 27 "bufo-juicy", 28 "good-news-bufo-offers-suppository", 29 "bufo-declines-your-suppository-offer", 30 "tsa-bufo-gropes-you", 31 ], 32 } 33 } 34} 35 36impl<T: Filterable> Filter<T> for BlocklistFilter { 37 fn matches(&self, item: &T) -> bool { 38 !self.blocklist.iter().any(|blocked| item.name().contains(blocked)) 39 } 40} 41 42/// filters out items matching any of the given regex patterns 43struct ExcludePatternFilter { 44 patterns: Vec<Regex>, 45} 46 47impl ExcludePatternFilter { 48 fn from_comma_separated(pattern_str: &str) -> Self { 49 let patterns = pattern_str 50 .split(',') 51 .map(|p| p.trim()) 52 .filter(|p| !p.is_empty()) 53 .filter_map(|p| Regex::new(p).ok()) 54 .collect(); 55 56 Self { patterns } 57 } 58 59 fn empty() -> Self { 60 Self { patterns: vec![] } 61 } 62} 63 64impl<T: Filterable> Filter<T> for ExcludePatternFilter { 65 fn matches(&self, item: &T) -> bool { 66 !self.patterns.iter().any(|p| p.is_match(item.name())) 67 } 68} 69 70/// combined filter that handles family-friendly mode and include/exclude patterns 71pub struct ContentFilter { 72 family_friendly: bool, 73 blocklist: BlocklistFilter, 74 exclude: ExcludePatternFilter, 75 include_patterns: Vec<Regex>, 76} 77 78impl ContentFilter { 79 pub fn new( 80 family_friendly: bool, 81 exclude_str: Option<&str>, 82 include_str: Option<&str>, 83 ) -> Self { 84 let exclude = exclude_str 85 .map(ExcludePatternFilter::from_comma_separated) 86 .unwrap_or_else(ExcludePatternFilter::empty); 87 88 let include_patterns: Vec<Regex> = include_str 89 .map(|s| { 90 s.split(',') 91 .map(|p| p.trim()) 92 .filter(|p| !p.is_empty()) 93 .filter_map(|p| Regex::new(p).ok()) 94 .collect() 95 }) 96 .unwrap_or_default(); 97 98 Self { 99 family_friendly, 100 blocklist: BlocklistFilter::inappropriate_bufos(), 101 exclude, 102 include_patterns, 103 } 104 } 105 106 pub fn exclude_pattern_count(&self) -> usize { 107 self.exclude.patterns.len() 108 } 109 110 pub fn exclude_patterns_str(&self) -> String { 111 self.exclude 112 .patterns 113 .iter() 114 .map(|r| r.as_str()) 115 .collect::<Vec<_>>() 116 .join(",") 117 } 118} 119 120impl<T: Filterable> Filter<T> for ContentFilter { 121 fn matches(&self, item: &T) -> bool { 122 // check family-friendly blocklist 123 if self.family_friendly && !self.blocklist.matches(item) { 124 return false; 125 } 126 127 // check if explicitly included (overrides exclude) 128 let matches_include = self.include_patterns.iter().any(|p| p.is_match(item.name())); 129 if matches_include { 130 return true; 131 } 132 133 // check exclude patterns 134 self.exclude.matches(item) 135 } 136} 137 138#[cfg(test)] 139mod tests { 140 use super::*; 141 142 struct TestItem { 143 name: String, 144 } 145 146 impl Filterable for TestItem { 147 fn name(&self) -> &str { 148 &self.name 149 } 150 } 151 152 #[test] 153 fn test_blocklist_filter() { 154 let filter = BlocklistFilter::inappropriate_bufos(); 155 let good = TestItem { 156 name: "bufo-happy".into(), 157 }; 158 let bad = TestItem { 159 name: "bufo-juicy".into(), 160 }; 161 162 assert!(filter.matches(&good)); 163 assert!(!filter.matches(&bad)); 164 } 165 166 #[test] 167 fn test_exclude_pattern_filter() { 168 let filter = ExcludePatternFilter::from_comma_separated("test, draft"); 169 let good = TestItem { 170 name: "bufo-happy".into(), 171 }; 172 let bad = TestItem { 173 name: "bufo-test-mode".into(), 174 }; 175 176 assert!(filter.matches(&good)); 177 assert!(!filter.matches(&bad)); 178 } 179 180 #[test] 181 fn test_include_overrides_exclude() { 182 let filter = ContentFilter::new(false, Some("party"), Some("birthday-party")); 183 let excluded = TestItem { 184 name: "bufo-party".into(), 185 }; 186 let included = TestItem { 187 name: "bufo-birthday-party".into(), 188 }; 189 190 assert!(!filter.matches(&excluded)); 191 assert!(filter.matches(&included)); 192 } 193}