lib/filter/
mod.rs

1//! Defines types for filtering.
2
3pub mod filters;
4
5use std::collections::BTreeSet;
6
7use crate::models::entry::Entries;
8
9/// A struct for running filters on [`Entry`][entry]s.
10///
11/// [entry]: crate::models::entry::Entry
12#[derive(Debug, Clone, Copy)]
13pub struct FilterRunner;
14
15impl FilterRunner {
16    /// Runs filters on all [`Entry`][entry]s.
17    ///
18    /// # Arguments
19    ///
20    /// * `filter_type` - The type of filter to run.
21    /// * `entries` - The [`Entry`][entry]s to filter.
22    ///
23    /// [entry]: crate::models::entry::Entry
24    pub fn run<F>(filter_type: F, entries: &mut Entries)
25    where
26        F: Into<FilterType>,
27    {
28        let filter_type: FilterType = filter_type.into();
29
30        match filter_type {
31            FilterType::Title { query, operator } => {
32                Self::filter_by_title(&query, operator, entries);
33            }
34            FilterType::Author { query, operator } => {
35                Self::filter_by_author(&query, operator, entries);
36            }
37            FilterType::Tags { query, operator } => {
38                Self::filter_by_tags(&query, operator, entries);
39            }
40        }
41
42        // Remove `Entry`s that have had all their `Annotation`s filtered out.
43        filters::contains_no_annotations(entries);
44    }
45
46    /// Filters out [`Entry`][entry]s by their [`Book::title`][book].
47    ///
48    /// # Arguments
49    ///
50    /// * `query` - A list of strings to filter against.
51    /// * `operator` - The [`FilterOperator`] to use.
52    /// * `entries` - The [`Entry`][entry]s to filter.
53    ///
54    /// [book]: crate::models::book::Book::title
55    /// [entry]: crate::models::entry::Entry
56    fn filter_by_title(query: &[String], operator: FilterOperator, entries: &mut Entries) {
57        match operator {
58            FilterOperator::Any => filters::by_title_any(query, entries),
59            FilterOperator::All => filters::by_title_all(query, entries),
60            FilterOperator::Exact => filters::by_title_exact(&query.join(" "), entries),
61        }
62    }
63
64    /// Filters out [`Entry`][entry]s by their [`Book::author`][book].
65    ///
66    /// # Arguments
67    ///
68    /// * `query` - A list of strings to filter against.
69    /// * `operator` - The [`FilterOperator`] to use.
70    /// * `entries` - The [`Entry`][entry]s to filter.
71    ///
72    /// [book]: crate::models::book::Book::author
73    /// [entry]: crate::models::entry::Entry
74    fn filter_by_author(query: &[String], operator: FilterOperator, entries: &mut Entries) {
75        match operator {
76            FilterOperator::Any => filters::by_author_any(query, entries),
77            FilterOperator::All => filters::by_author_all(query, entries),
78            FilterOperator::Exact => filters::by_author_exact(&query.join(" "), entries),
79        }
80    }
81
82    /// Filters out [`Entry`][entry]s by their [`tags`][tags].
83    ///
84    /// # Arguments
85    ///
86    /// * `query` - A list of strings to filter against.
87    /// * `operator` - The [`FilterOperator`] to use.
88    /// * `entries` - The [`Entry`][entry]s to filter.
89    ///
90    /// [entry]: crate::models::entry::Entry
91    /// [tags]: crate::models::annotation::Annotation::tags
92    fn filter_by_tags(query: &[String], operator: FilterOperator, entries: &mut Entries) {
93        let tags = BTreeSet::from_iter(query);
94
95        match operator {
96            FilterOperator::Any => filters::by_tags_any(&tags, entries),
97            FilterOperator::All => filters::by_tags_all(&tags, entries),
98            FilterOperator::Exact => filters::by_tags_exact(&tags, entries),
99        }
100    }
101}
102
103/// An enum representing possible filter types.
104///
105/// A filter generally consists of three elements: (1) the field to use for filtering, (2) a list of
106/// queries and (3) a [`FilterOperator`] to determine how to handle the queries.
107#[derive(Debug, Clone)]
108pub enum FilterType {
109    /// Sets the filter to use the [`Book::title`][book] field for filtering.
110    ///
111    /// [book]: crate::models::book::Book::title
112    Title {
113        #[allow(missing_docs)]
114        query: Vec<String>,
115        #[allow(missing_docs)]
116        operator: FilterOperator,
117    },
118
119    /// Sets the filter to use the [`Book::author`][book] field for filtering.
120    ///
121    /// [book]: crate::models::book::Book::author
122    Author {
123        #[allow(missing_docs)]
124        query: Vec<String>,
125        #[allow(missing_docs)]
126        operator: FilterOperator,
127    },
128
129    /// Sets the filter to use the [`Annotation::tags`][annotation] field for filtering.
130    ///
131    /// [annotation]: crate::models::annotation::Annotation::tags
132    Tags {
133        #[allow(missing_docs)]
134        query: Vec<String>,
135        #[allow(missing_docs)]
136        operator: FilterOperator,
137    },
138}
139
140#[cfg(test)]
141impl FilterType {
142    fn title(query: &[&str], operator: FilterOperator) -> Self {
143        Self::Title {
144            query: query.iter().map(std::string::ToString::to_string).collect(),
145            operator,
146        }
147    }
148
149    fn author(query: &[&str], operator: FilterOperator) -> Self {
150        Self::Author {
151            query: query.iter().map(std::string::ToString::to_string).collect(),
152            operator,
153        }
154    }
155
156    fn tags(query: &[&str], operator: FilterOperator) -> Self {
157        Self::Tags {
158            query: query.iter().map(std::string::ToString::to_string).collect(),
159            operator,
160        }
161    }
162}
163
164/// An enum representing possible filter operators.
165///
166/// See [`FilterType`] for more information.
167#[derive(Debug, Clone, Copy, Default)]
168pub enum FilterOperator {
169    /// Sets the filter to check if any of the queries match.
170    #[default]
171    Any,
172
173    /// Sets the filter to check if all of the queries match.
174    All,
175
176    /// Sets the filter to check if the query string is an exact match.
177    Exact,
178}
179
180#[cfg(test)]
181mod test {
182
183    use super::*;
184
185    use std::collections::HashMap;
186
187    use crate::models::annotation::Annotation;
188    use crate::models::book::Book;
189    use crate::models::entry::Entry;
190
191    fn create_test_entries() -> Entries {
192        let annotations = vec![
193            Annotation {
194                tags: create_test_tags(&["#tag01"]),
195                ..Default::default()
196            },
197            Annotation {
198                tags: create_test_tags(&["#tag02"]),
199                ..Default::default()
200            },
201            Annotation {
202                tags: create_test_tags(&["#tag03"]),
203                ..Default::default()
204            },
205            Annotation {
206                tags: create_test_tags(&["#tag01", "#tag02", "#tag03"]),
207                ..Default::default()
208            },
209        ];
210
211        let entry_00 = Entry {
212            book: Book {
213                title: "Incididunt Sint".to_string(),
214                author: "Quis Sint".to_string(),
215                ..Default::default()
216            },
217            annotations: annotations.clone(),
218        };
219
220        // Laboris Incididunt Esse Commodo Do Tempor Ut
221        // Lorem aliqua do ex cillum
222        let entry_01 = Entry {
223            book: Book {
224                title: "Laboris Ex Cillum".to_string(),
225                author: "Lorem Du Quis".to_string(),
226                ..Default::default()
227            },
228            annotations,
229        };
230
231        let mut data = HashMap::new();
232        data.insert("00".to_string(), entry_00);
233        data.insert("01".to_string(), entry_01);
234
235        data
236    }
237
238    fn create_test_tags(tags: &[&str]) -> BTreeSet<String> {
239        tags.iter().map(std::string::ToString::to_string).collect()
240    }
241
242    // Keeps annotations where their book's title contains "incididunt" or "laboris".
243    #[test]
244    fn title_any() {
245        let mut entries = create_test_entries();
246
247        FilterRunner::run(
248            FilterType::title(&["incididunt", "laboris"], FilterOperator::Any),
249            &mut entries,
250        );
251
252        let annotations = entries
253            .values()
254            .flat_map(|entry| &entry.annotations)
255            .count();
256
257        assert_eq!(entries.len(), 2);
258        assert_eq!(annotations, 8);
259    }
260
261    // Keeps annotations where their book's title contains both "laboris" and "cillum".
262    #[test]
263    fn title_all() {
264        let mut entries = create_test_entries();
265
266        FilterRunner::run(
267            FilterType::title(&["laboris", "cillum"], FilterOperator::All),
268            &mut entries,
269        );
270
271        let annotations = entries
272            .values()
273            .flat_map(|entry| &entry.annotations)
274            .count();
275
276        assert_eq!(entries.len(), 1);
277        assert_eq!(annotations, 4);
278    }
279
280    // Keeps annotations where their book's title is exactly "incididunt sint".
281    #[test]
282    fn title_exact() {
283        let mut entries = create_test_entries();
284
285        FilterRunner::run(
286            FilterType::title(&["incididunt", "sint"], FilterOperator::Exact),
287            &mut entries,
288        );
289
290        let annotations = entries
291            .values()
292            .flat_map(|entry| &entry.annotations)
293            .count();
294
295        assert_eq!(entries.len(), 1);
296        assert_eq!(annotations, 4);
297    }
298
299    // Keeps annotations where their book's author contains "quis".
300    #[test]
301    fn author_any() {
302        let mut entries = create_test_entries();
303
304        FilterRunner::run(
305            FilterType::author(&["quis"], FilterOperator::Any),
306            &mut entries,
307        );
308
309        let annotations = entries
310            .values()
311            .flat_map(|entry| &entry.annotations)
312            .count();
313
314        assert_eq!(entries.len(), 2);
315        assert_eq!(annotations, 8);
316    }
317
318    // Keeps annotations where their book's author contains both "lorem" and "sint".
319    #[test]
320    fn author_all() {
321        let mut entries = create_test_entries();
322
323        FilterRunner::run(
324            FilterType::author(&["lorem", "sint"], FilterOperator::All),
325            &mut entries,
326        );
327
328        let annotations = entries
329            .values()
330            .flat_map(|entry| &entry.annotations)
331            .count();
332
333        assert_eq!(entries.len(), 0);
334        assert_eq!(annotations, 0);
335    }
336
337    // Keeps annotations where their book's author is exactly "lorem du quis".
338    #[test]
339    fn author_exact() {
340        let mut entries = create_test_entries();
341
342        FilterRunner::run(
343            FilterType::author(&["lorem", "du", "quis"], FilterOperator::Exact),
344            &mut entries,
345        );
346
347        let annotations = entries
348            .values()
349            .flat_map(|entry| &entry.annotations)
350            .count();
351
352        assert_eq!(entries.len(), 1);
353        assert_eq!(annotations, 4);
354    }
355
356    // Keeps annotations where their tags contain "#tag01" or "#tag03".
357    #[test]
358    fn tags_any() {
359        let mut entries = create_test_entries();
360
361        FilterRunner::run(
362            FilterType::tags(&["#tag01", "#tag03"], FilterOperator::Any),
363            &mut entries,
364        );
365
366        let annotations = entries
367            .values()
368            .flat_map(|entry| &entry.annotations)
369            .count();
370
371        assert_eq!(entries.len(), 2);
372        assert_eq!(annotations, 6);
373    }
374
375    // Keeps annotations where their tags contain both "#tag01" and "#tag03".
376    #[test]
377    fn tags_all() {
378        let mut entries = create_test_entries();
379
380        FilterRunner::run(
381            FilterType::tags(&["#tag01", "#tag03"], FilterOperator::All),
382            &mut entries,
383        );
384
385        let annotations = entries
386            .values()
387            .flat_map(|entry| &entry.annotations)
388            .count();
389
390        assert_eq!(entries.len(), 2);
391        assert_eq!(annotations, 2);
392    }
393
394    // Keeps annotations where their tags contain exactly "#tag01", "#tag02" and "#tag03".
395    #[test]
396    fn tags_exact() {
397        let mut entries = create_test_entries();
398
399        FilterRunner::run(
400            FilterType::tags(&["#tag01", "#tag02", "#tag03"], FilterOperator::Exact),
401            &mut entries,
402        );
403
404        let annotations = entries
405            .values()
406            .flat_map(|entry| &entry.annotations)
407            .count();
408
409        assert_eq!(entries.len(), 2);
410        assert_eq!(annotations, 2);
411    }
412
413    // Tests that tag declaration order doesn't matter when performing exact match filtering.
414    #[test]
415    fn tags_exact_different_order() {
416        let mut entries = create_test_entries();
417
418        FilterRunner::run(
419            FilterType::tags(&["#tag03", "#tag02", "#tag01"], FilterOperator::Exact),
420            &mut entries,
421        );
422
423        let annotations = entries
424            .values()
425            .flat_map(|entry| &entry.annotations)
426            .count();
427
428        assert_eq!(entries.len(), 2);
429        assert_eq!(annotations, 2);
430    }
431
432    // Tests that multiple filters produce the expected result.
433    #[test]
434    fn multi() {
435        let mut entries = create_test_entries();
436
437        FilterRunner::run(
438            FilterType::title(&["sint"], FilterOperator::Any),
439            &mut entries,
440        );
441
442        FilterRunner::run(
443            FilterType::author(&["quis", "sint"], FilterOperator::Exact),
444            &mut entries,
445        );
446
447        FilterRunner::run(
448            FilterType::tags(&["#tag02"], FilterOperator::Any),
449            &mut entries,
450        );
451
452        let annotations = entries
453            .values()
454            .flat_map(|entry| &entry.annotations)
455            .count();
456
457        assert_eq!(entries.len(), 1);
458        assert_eq!(annotations, 2);
459    }
460}