Skip to main content

git_su/
user_list.rs

1// UserList: find users by initials, name segment, etc.
2
3use crate::user::User;
4use crate::user_file::UserFile;
5
6pub struct UserList {
7    user_file: UserFile,
8}
9
10impl UserList {
11    pub fn new(user_file: UserFile) -> Self {
12        UserList { user_file }
13    }
14
15    pub fn add(&self, user: &User) {
16        let _ = self.user_file.write(user);
17    }
18
19    pub fn list(&self) -> Vec<User> {
20        self.user_file.read()
21    }
22
23    /// Find a unique combination of users matching the given search terms.
24    pub fn find(&self, search_terms: &[String]) -> Result<Vec<User>, String> {
25        let all = self.list();
26        if search_terms.is_empty() {
27            return Ok(vec![]);
28        }
29        let mut matches_per_term: Vec<Vec<User>> = Vec::new();
30        for term in search_terms {
31            let m = Self::matching_users(&all, term);
32            if m.is_empty() {
33                return Err(format!("No user found matching '{}'", term));
34            }
35            matches_per_term.push(m);
36        }
37        Self::unique_combination(&matches_per_term)
38            .ok_or_else(|| {
39                format!(
40                    "Couldn't find a combination of users matching {}",
41                    search_terms
42                        .iter()
43                        .map(|s| format!("'{}'", s))
44                        .collect::<Vec<_>>()
45                        .join(", ")
46                )
47            })
48    }
49
50    fn matching_users(all: &[User], search_term: &str) -> Vec<User> {
51        let term_lower = search_term.to_lowercase();
52        let mut result: Vec<User> = all
53            .iter()
54            .filter(|u| {
55                // Whole word of name
56                u.name()
57                    .split_whitespace()
58                    .any(|w| w.to_lowercase() == term_lower)
59                    || u.name().to_lowercase().split_whitespace().any(|w| {
60                        w.starts_with(&term_lower) || term_lower.starts_with(&w.to_lowercase())
61                    })
62                    || u.initials().contains(&term_lower)
63                    || u.name().to_lowercase().contains(&term_lower)
64                    || u.email().to_lowercase().contains(&term_lower)
65            })
66            .cloned()
67            .collect();
68        result.dedup();
69        result
70    }
71
72    fn unique_combination(term_matches: &[Vec<User>]) -> Option<Vec<User>> {
73        let mut combinations: Vec<Vec<User>> = vec![vec![]];
74        for matches in term_matches {
75            let mut new_combos = Vec::new();
76            for combo in &combinations {
77                for u in matches {
78                    if !combo.iter().any(|c| c == u) {
79                        let mut extended = combo.clone();
80                        extended.push(u.clone());
81                        new_combos.push(extended);
82                    }
83                }
84            }
85            combinations = new_combos;
86        }
87        combinations.into_iter().find(|c| c.len() == term_matches.len())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::user_file::UserFile;
95    use std::fs;
96
97    fn list_with_users(users: &[(&str, &str)]) -> (UserList, tempfile::TempDir) {
98        let dir = tempfile::tempdir().unwrap();
99        let path = dir.path().join(".git-su");
100        fs::write(&path, "").unwrap();
101        let uf = UserFile::new(&path);
102        for (name, email) in users {
103            uf.write(&User::new(*name, *email)).unwrap();
104        }
105        (UserList::new(uf), dir)
106    }
107
108    #[test]
109    fn find_by_initials() {
110        let (list, _dir) = list_with_users(&[
111            ("Jane Doe", "jane@example.com"),
112            ("Bob Smith", "bob@example.com"),
113        ]);
114        let found = list.find(&["jd".to_string()]).unwrap();
115        assert_eq!(found.len(), 1);
116        assert_eq!(found[0].name(), "Jane Doe");
117    }
118
119    #[test]
120    fn find_by_name_part() {
121        let (list, _dir) = list_with_users(&[("Alice Cooper", "alice@example.com")]);
122        let found = list.find(&["alice".to_string()]).unwrap();
123        assert_eq!(found.len(), 1);
124        assert_eq!(found[0].name(), "Alice Cooper");
125    }
126
127    #[test]
128    fn find_no_match() {
129        let (list, _dir) = list_with_users(&[("Jane Doe", "jane@example.com")]);
130        let r = list.find(&["xyz".to_string()]);
131        assert!(r.is_err());
132    }
133
134    #[test]
135    fn find_pair_two_terms() {
136        let (list, _dir) = list_with_users(&[
137            ("Jane Doe", "jane@example.com"),
138            ("Bob Smith", "bob@example.com"),
139        ]);
140        let found = list.find(&["jd".to_string(), "bob".to_string()]).unwrap();
141        assert_eq!(found.len(), 2);
142        assert_eq!(found[0].name(), "Jane Doe");
143        assert_eq!(found[1].name(), "Bob Smith");
144    }
145
146    #[test]
147    fn list_returns_added_users() {
148        let (list, _dir) = list_with_users(&[("Jane Doe", "jane@example.com")]);
149        let all = list.list();
150        assert_eq!(all.len(), 1);
151        assert_eq!(all[0].email(), "jane@example.com");
152    }
153}