libzettels/backstage/querying/
mod.rs

1//Copyright (c) 2020-2022 Stefan Thesing
2//
3//This file is part of libzettels.
4//
5//libzettels is free software: you can redistribute it and/or modify
6//it under the terms of the GNU General Public License as published by
7//the Free Software Foundation, either version 3 of the License, or
8//(at your option) any later version.
9//
10//libzettels is distributed in the hope that it will be useful,
11//but WITHOUT ANY WARRANTY; without even the implied warranty of
12//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13//GNU General Public License for more details.
14//
15//You should have received a copy of the GNU General Public License
16//along with Zettels. If not, see http://www.gnu.org/licenses/.
17
18//! Module for querying the index.
19
20// --------------------------------------------------------------------------
21
22use std::path::{PathBuf};
23use std::collections::{HashSet, BTreeMap};
24use backstage::index::Index;
25use backstage::zettel::Zettel;
26pub mod sequences;
27
28// --------------------------------------------------------------------------
29// Search
30// --------------------------------------------------------------------------
31// Keywords
32
33/// Takes a reference to the index and returns a list of all used keywords
34/// (as a HashSet).
35/// Called by a wrapper function of Index. See there for details.
36pub fn get_keywords(index: &Index) -> HashSet<String> {
37    let mut keywords = HashSet::new();
38    for (_, zettel) in &index.files {
39        for keyword in &zettel.keywords {
40            keywords.insert(keyword.to_string());
41        }
42    }
43    keywords
44}
45
46/// Takes a reference to the index and returns a list containing the number
47/// of occurences of every keywords (as a BTreeMap).
48/// Called by a wrapper function of Index. See there for details.
49pub fn count_keywords(index: &Index) -> BTreeMap<String, u32> {
50    let mut counted_keywords = BTreeMap::new();
51    for (_, zettel) in &index.files {
52        for keyword in &zettel.keywords {
53            if let Some(count) = counted_keywords.get(keyword) {
54                let new_count = count + 1;
55                counted_keywords.insert(keyword.to_string(), new_count);
56            } else {
57                counted_keywords.insert(keyword.to_string(), 1);
58            }
59        }
60    }
61    counted_keywords
62}
63
64/// Takes a reference to the index and a vector of Strings with keywords. 
65/// Returns a list of all zettels that have one or – if `all` is true – 
66/// all these keywords.
67/// Called by a wrapper function of Index. See there for details.
68pub fn search_index_for_keywords(index: &Index, 
69                                searched_keywords: Vec<String>,
70                                all: bool) -> HashSet<PathBuf> {
71    let mut matching_zettels = HashSet::new();
72    for (key, zettel) in &index.files {
73        if search_zettel_for_keywords(&zettel, &searched_keywords, all) {
74            matching_zettels.insert(key.to_path_buf());
75        }
76    }
77    matching_zettels
78}
79
80/// Takes a reference to a Zettel `zettel` and a vector of Strings. Returns 
81/// `true` if the zettel lists one of the strings as a keyword, or – if `all`  
82/// is true – if the zettel lists all of them as keywords.
83// only called by `search_index_for_keywords`.
84fn search_zettel_for_keywords(zettel: &Zettel, 
85              searched_keywords: &Vec<String>,
86              all: bool) -> bool {
87    if all {
88        // All of searched_keywords need to be in zettel.keywords
89        for keyword in searched_keywords {
90            // so if it is NOT in, return false
91            if !zettel.keywords.contains(&keyword) {
92                return false;
93            }
94        }
95        // still here? So all are in.
96        return true;
97    } else {
98        // Just one of the searched_keywords need to be in zettel.keywords
99        for keyword in searched_keywords {
100            // so if it is in, return true
101            if zettel.keywords.contains(&keyword) {
102                return true;
103            }
104        }
105        // No match, so:
106        return false;
107    }
108}
109
110// Title
111/// Searches the index for zettels whose title matches `search_term` (exactly
112/// if `exact` is true). Returns a list of matching zettels.
113/// Called by a wrapper function of Index. See there for details.
114pub fn search_index_for_title<T: AsRef<str>>(index: &Index, 
115                                            search_term: T,
116                                            exact: bool) -> HashSet<PathBuf> {
117    let mut matching_zettels = HashSet::new();
118    for (key, zettel) in &index.files {
119        if search_zettel_title(&zettel, &search_term, exact) {
120            matching_zettels.insert(key.to_path_buf());
121        }
122    }
123    matching_zettels
124}
125
126/// Searches the title. Returns `true` if the referenced zettel's title  
127/// contains the search term, or – if `exact` is true, if it exactly matches 
128/// the search term.
129fn search_zettel_title<T: AsRef<str>>(zettel: &Zettel,
130                              search_term: T,
131                              exact: bool) -> bool {
132    let search_term = search_term.as_ref();
133    let qs = search_term;
134    let zt = zettel.title.as_str();
135    
136    if exact {
137        trace!("exact. Returning {:?}", zt==qs);
138        zt == qs
139    } else {
140        let qs = qs.to_ascii_lowercase();
141        let zt = zt.to_ascii_lowercase();
142        zt.contains(&qs)
143    }
144}
145
146
147/// Combines searches for keywords and title. If `all` is true, 
148/// zettels must match all search criteria.
149pub fn combi_search<T: AsRef<str>>(index: &Index, 
150                                   searched_keywords: Vec<String>,
151                                   all: bool, 
152                                   search_term: T,
153                                   exact: bool) 
154                                   -> HashSet<PathBuf> {
155    let mut k_result = search_index_for_keywords(&index, 
156                                             searched_keywords,
157                                             all);
158    let t_result = search_index_for_title(&index, search_term, exact);
159    
160    // Let's see if our result need to match *all* criteria
161    if all {
162        // We want only entries that are present in both results
163        let mut temp = HashSet::new();
164        for r in k_result {
165            if t_result.contains(&r) {
166                temp.insert(r);
167            }
168        }
169        k_result = temp;
170    } else {
171        // We can just add t_result to k_result and return the latter
172        for r in t_result {
173            k_result.insert(r);
174        }
175    }
176    k_result
177}
178// Content
179// I won't implement a search for content. That's a classic job for grep or 
180// ripgrep…
181
182// --------------------------------------------------------------------------
183// Inspect
184// --------------------------------------------------------------------------
185// The following functions for inspection are in module `sequences`:
186// - `sequences`
187// - `zettels_of_sequence`
188// - `sequence_tree`
189// - `sequence_tree_whole`
190// 
191// The following functions inspections are in module `zettel`:
192// - `links_to`
193//
194// That leaves the one for incoming links. That one is here:
195
196/// Takes a reference to the index as well as a list of zettels whose 
197/// incoming links are to be inspected.
198/// Returns a HashSet with the keys of other zettels linking to one (or all)
199/// of the zettels, as by the `all` parameter.
200pub fn inspect_incoming(index: &Index, 
201                    inspected_zettels: &HashSet<PathBuf>, 
202                    all: bool) 
203                    -> HashSet<PathBuf> {
204    let mut incoming = HashSet::new();
205    // iterate over everything.
206    for (other_key, other_zettel) in &index.files {
207        if other_zettel.links_to(&inspected_zettels, all) {
208            incoming.insert(other_key.to_path_buf());
209        }
210    }
211    incoming
212}
213
214// --------------------------------------------------------------------------
215#[cfg(test)]
216mod tests {
217    use super::*;
218    extern crate tempfile;
219    use self::tempfile::tempdir;
220    use examples::*;
221    use std::path::Path;
222    
223    fn setup() -> Index {
224        let tmp_dir = tempdir().expect("Failed setting up temp directory.");
225        let dir = tmp_dir.path();
226        generate_examples_with_index(dir)
227            .expect("Failed to generate examples");
228        let file_path = dir.join("examples/config/index.yaml");
229        let index = Index::from_file(&file_path).unwrap();
230        index
231    }
232    
233    #[test]
234    fn test_search_zettel_for_keywords() {
235        // Setup
236        let index = setup();
237        let z1 = index.get_zettel(Path::new("file1.md"));
238        assert!(z1.is_some());
239        let z1 = z1.unwrap();
240        // Note: z1 has the following keywords: example, first, test
241        
242        // Tests:
243        // One searched, contained, not all
244        let searched_keywords = vec!["example".to_string()];
245        assert!(search_zettel_for_keywords(&z1, &searched_keywords, false));
246        
247        // One searched, not contained, not all
248        let searched_keywords = vec!["foo".to_string()];
249        assert!(!search_zettel_for_keywords(&z1, &searched_keywords, false));
250        
251        // Two searched, one contained, not all
252        let searched_keywords = vec!["example".to_string(),
253                                     "foo".to_string()];
254        assert!(search_zettel_for_keywords(&z1, &searched_keywords, false));
255        
256        // Two searched, none contained, not all
257        let searched_keywords = vec!["foo".to_string(),
258                                     "bar".to_string()];
259        assert!(!search_zettel_for_keywords(&z1, &searched_keywords, false));
260        
261        // Two searched, one contained, all
262        let searched_keywords = vec!["example".to_string(),
263                                     "foo".to_string()];
264        assert!(!search_zettel_for_keywords(&z1, &searched_keywords, true));
265        
266        // Two searched, two contained, all
267        let searched_keywords = vec!["example".to_string(),
268                                     "first".to_string()];
269        assert!(search_zettel_for_keywords(&z1, &searched_keywords, true));
270        
271        // Three searched, two contained, not all
272        let searched_keywords = vec!["example".to_string(),
273                                     "first".to_string(),
274                                     "foo".to_string()];
275        assert!(search_zettel_for_keywords(&z1, &searched_keywords, false));
276        
277        // Three searched, two contained, all
278        let searched_keywords = vec!["example".to_string(),
279                                     "first".to_string(),
280                                     "foo".to_string()];
281        assert!(!search_zettel_for_keywords(&z1, &searched_keywords, true));
282    }
283    
284    #[test]
285    fn test_get_keywords() {
286        let index = setup();
287        
288        let keywords = get_keywords(&index);
289        assert_eq!(keywords.len(), 7);
290        assert!(keywords.contains("example"));
291        assert!(keywords.contains("test"));
292        assert!(keywords.contains("first"));
293        assert!(keywords.contains("second"));
294        assert!(keywords.contains("third"));        
295        assert!(keywords.contains("fourth"));        
296        assert!(keywords.contains("pureyaml"));        
297    }
298    
299    #[test]
300    fn test_count_keywords() {
301        let index = setup();
302        
303        let keyword_count = count_keywords(&index);
304        assert_eq!(Some(&5), keyword_count.get("example"));
305        assert_eq!(Some(&1), keyword_count.get("test"));
306        assert_eq!(Some(&1), keyword_count.get("first"));
307        assert_eq!(Some(&1), keyword_count.get("second"));
308        assert_eq!(Some(&1), keyword_count.get("third"));
309        assert_eq!(Some(&1), keyword_count.get("fourth"));
310        assert_eq!(Some(&1), keyword_count.get("pureyaml"));
311    }
312    
313    #[test]
314    fn test_search_index_for_keywords() {
315        let index = setup();
316        
317        // One searched, contained, not all
318        let searched_keywords = vec!["first".to_string()];
319        let matching_zettels = search_index_for_keywords(&index, 
320                                                         searched_keywords,
321                                                         false);
322        assert_eq!(matching_zettels.len(), 1);
323        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
324        
325        // Two searched, one contained, not all
326        let searched_keywords = vec!["first".to_string(),
327                                     "foo".to_string()];
328        let matching_zettels = search_index_for_keywords(&index, 
329                                                         searched_keywords,
330                                                         false);
331        assert_eq!(matching_zettels.len(), 1);
332        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
333        
334        // Two searched, one contained, all
335        let searched_keywords = vec!["first".to_string(),
336                                     "foo".to_string()];
337        let matching_zettels = search_index_for_keywords(&index, 
338                                                         searched_keywords,
339                                                         true);
340        assert_eq!(matching_zettels.len(), 0);
341   
342    
343        // Two searched, one contained in one zettel each, not all
344        let searched_keywords = vec!["first".to_string(),
345                                     "second".to_string()];
346        let matching_zettels = search_index_for_keywords(&index, 
347                                                         searched_keywords,
348                                                         false);
349        assert_eq!(matching_zettels.len(), 2);
350        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
351        assert!(matching_zettels.contains(&PathBuf::from("file2.md")));
352        
353        // Two searched, one contained in one zettel each, all
354        let searched_keywords = vec!["first".to_string(),
355                                     "second".to_string()];
356        let matching_zettels = search_index_for_keywords(&index, 
357                                                         searched_keywords,
358                                                         true);
359        assert_eq!(matching_zettels.len(), 0);
360        
361        // Two searched, two contained in one zettel, others have only one, all
362        let searched_keywords = vec!["first".to_string(),
363                                     "example".to_string()];
364        let matching_zettels = search_index_for_keywords(&index, 
365                                                         searched_keywords,
366                                                         true);
367        assert_eq!(matching_zettels.len(), 1);
368        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
369    }
370    
371    #[test]
372    fn test_search_zettel_title() {
373        // Setup
374        let index = setup();
375        let z1 = index.get_zettel(Path::new("file1.md"));
376        assert!(z1.is_some());
377        let z1 = z1.unwrap();
378        
379        // Test full match, not exact
380        assert!(search_zettel_title(&z1, "File 1", false));
381        
382        // Test full match, exact
383        assert!(search_zettel_title(&z1, "File 1", true));
384        
385        // Test partial match, not exact
386        assert!(search_zettel_title(&z1, "File", false));
387        
388        // Test partial match, exact
389        assert!(!search_zettel_title(&z1, "File", true));
390        
391        // Test no match, not exact
392        assert!(!search_zettel_title(&z1, "foo", false));
393        
394        // Test no match, exact
395        assert!(!search_zettel_title(&z1, "foo", true));
396    }
397    
398    #[test]
399    fn test_search_index_for_title() {
400        let index = setup();
401        
402        // Test full match for one, not exact
403        let matching_zettels = search_index_for_title(&index, "File 1", false);
404        assert_eq!(matching_zettels.len(), 1);
405        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
406        
407        // Test full match for one, exact
408        let matching_zettels = search_index_for_title(&index, "File 1", true);
409        assert_eq!(matching_zettels.len(), 1);
410        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
411        
412        // Test partial match for five, not exact
413        let matching_zettels = search_index_for_title(&index, "File", false);
414        assert_eq!(matching_zettels.len(), 5);
415        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
416        assert!(matching_zettels.contains(&PathBuf::from("file2.md")));
417        assert!(matching_zettels.contains(&PathBuf::from("file3.md")));
418        assert!(matching_zettels.contains(&PathBuf::from("subdir/file4.md")));
419        assert!(matching_zettels.contains(&PathBuf::from("subdir/file5.md")));
420        
421        // Test partial match for five, exact
422        let matching_zettels = search_index_for_title(&index, "File", true);
423        assert_eq!(matching_zettels.len(), 0);
424        
425        // Test no match, not exact
426        let matching_zettels = search_index_for_title(&index, "foo", false);
427        assert_eq!(matching_zettels.len(), 0);
428        
429        // Test no match, exact
430        let matching_zettels = search_index_for_title(&index, "foo", true);
431        assert_eq!(matching_zettels.len(), 0);
432    }
433    
434    #[test]
435    fn test_combi_search() {
436        let index = setup();
437        
438        let searched_keywords = vec!["first".to_string()];
439        
440        let matching_zettels = combi_search(&index, 
441                                            searched_keywords.clone(), 
442                                            true, 
443                                            "File", 
444                                            true);
445        assert_eq!(matching_zettels.len(), 0);
446        
447        let matching_zettels = combi_search(&index, 
448                                            searched_keywords.clone(), 
449                                            true, 
450                                            "File", 
451                                            false);
452        assert_eq!(matching_zettels.len(), 1);
453        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
454        
455        let matching_zettels = combi_search(&index, 
456                                            searched_keywords, 
457                                            true, 
458                                            "File 1", 
459                                            true);
460        assert_eq!(matching_zettels.len(), 1);
461        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
462        
463        let searched_keywords = vec!["example".to_string()];
464        
465        let matching_zettels = combi_search(&index, 
466                                            searched_keywords.clone(), 
467                                            true, 
468                                            "File 1", 
469                                            true);
470        assert_eq!(matching_zettels.len(), 1);
471        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
472        
473        let matching_zettels = combi_search(&index, 
474                                            searched_keywords, 
475                                            false, 
476                                            "File 1", 
477                                            true);
478        assert_eq!(matching_zettels.len(), 5);
479        assert!(matching_zettels.contains(&PathBuf::from("file1.md")));
480        assert!(matching_zettels.contains(&PathBuf::from("file2.md")));
481        assert!(matching_zettels.contains(&PathBuf::from("file3.md")));
482        assert!(matching_zettels.contains(&PathBuf::from("subdir/file4.md")));
483        assert!(matching_zettels.contains(&PathBuf::from("onlies/pure-yaml.yaml")));
484    }
485
486    #[test]
487    fn test_inspect_incoming() {
488        // Setup
489        let index = setup();
490        let mut inspected_zettels = HashSet::new();
491        inspected_zettels.insert(PathBuf::from("file2.md"));
492        inspected_zettels.insert(PathBuf::from("file3.md"));
493        
494        // Note:
495        // file1 links to both
496        // file2 links to file3
497        // onlies/markdown-only links to file2
498        
499        // Test: What links to either of them?
500        let incoming = inspect_incoming(&index, &inspected_zettels, false);
501        assert_eq!(incoming.len(), 3);
502        assert!(incoming.contains(&PathBuf::from("file1.md")));
503        assert!(incoming.contains(&PathBuf::from("file2.md")));
504        assert!(incoming.contains(&PathBuf::from("onlies/markdown-only.md")));
505        
506        // Test: What links to both of them?
507        let incoming = inspect_incoming(&index, &inspected_zettels, true);
508        assert_eq!(incoming.len(), 1);
509        assert!(incoming.contains(&PathBuf::from("file1.md")));
510        
511        // What if we also search for links a non-existent file?
512        let mut inspected_zettels = HashSet::new();
513        inspected_zettels.insert(PathBuf::from("file2.md"));
514        inspected_zettels.insert(PathBuf::from("file42.md"));
515
516        
517        // Test: What links to either of them?                            
518        let incoming = inspect_incoming(&index, &inspected_zettels, false);
519        assert_eq!(incoming.len(), 2);
520        assert!(incoming.contains(&PathBuf::from("file1.md")));
521        assert!(incoming.contains(&PathBuf::from("onlies/markdown-only.md")));
522        
523        // Test: What links to both of them?                            
524        let incoming = inspect_incoming(&index, &inspected_zettels, true);
525        assert_eq!(incoming.len(), 0);
526    }
527}