1use crate::contracts::{QueueFile, Task, TaskStatus};
19use crate::queue::search::normalize::normalize;
20use std::collections::HashSet;
21
22pub fn filter_tasks<'a>(
23 queue: &'a QueueFile,
24 statuses: &[TaskStatus],
25 tags: &[String],
26 scopes: &[String],
27 limit: Option<usize>,
28) -> Vec<&'a Task> {
29 let status_filter: HashSet<TaskStatus> = statuses.iter().copied().collect();
30 let tag_filter: HashSet<String> = tags
31 .iter()
32 .map(|tag| normalize(tag))
33 .filter(|tag| !tag.is_empty())
34 .collect();
35 let scope_filter: Vec<String> = scopes
36 .iter()
37 .map(|scope| normalize(scope))
38 .filter(|scope| !scope.is_empty())
39 .collect();
40
41 let has_status_filter = !status_filter.is_empty();
42 let has_tag_filter = !tag_filter.is_empty();
43 let has_scope_filter = !scope_filter.is_empty();
44
45 let mut out = Vec::new();
46 for task in &queue.tasks {
47 if has_status_filter && !status_filter.contains(&task.status) {
48 continue;
49 }
50 if has_tag_filter
51 && !task
52 .tags
53 .iter()
54 .any(|tag| tag_filter.contains(&normalize(tag)))
55 {
56 continue;
57 }
58 if has_scope_filter
59 && !task.scope.iter().any(|scope| {
60 let hay = normalize(scope);
61 scope_filter.iter().any(|needle| hay.contains(needle))
62 })
63 {
64 continue;
65 }
66
67 out.push(task);
68 if let Some(limit) = limit
69 && out.len() >= limit
70 {
71 break;
72 }
73 }
74 out
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::contracts::{QueueFile, TaskStatus};
81 use crate::queue::search::test_support::{task_with_scope, task_with_tags_scope_status};
82
83 #[test]
84 fn filter_tasks_with_scope_filter() {
85 let queue = QueueFile {
86 version: 1,
87 tasks: vec![
88 task_with_scope("RQ-0001", vec!["crates/ralph".to_string()]),
89 task_with_scope("RQ-0002", vec!["docs/cli".to_string()]),
90 task_with_scope("RQ-0003", vec!["crates/auth".to_string()]),
91 ],
92 };
93
94 let results = filter_tasks(&queue, &[], &[], &["crates/ralph".to_string()], None);
95 assert_eq!(results.len(), 1);
96 assert_eq!(results[0].id, "RQ-0001");
97 }
98
99 #[test]
100 fn filter_tasks_scope_filter_case_insensitive() {
101 let queue = QueueFile {
102 version: 1,
103 tasks: vec![
104 task_with_scope("RQ-0001", vec!["CRATES/ralph".to_string()]),
105 task_with_scope("RQ-0002", vec!["docs/cli".to_string()]),
106 ],
107 };
108
109 let results = filter_tasks(&queue, &[], &[], &["crates/ralph".to_string()], None);
110 assert_eq!(results.len(), 1);
111 assert_eq!(results[0].id, "RQ-0001");
112 }
113
114 #[test]
115 fn filter_tasks_scope_filter_substring() {
116 let queue = QueueFile {
117 version: 1,
118 tasks: vec![
119 task_with_scope("RQ-0001", vec!["crates/ralph/src/cli".to_string()]),
120 task_with_scope("RQ-0002", vec!["docs/cli".to_string()]),
121 task_with_scope("RQ-0003", vec!["crates/auth".to_string()]),
122 ],
123 };
124
125 let results = filter_tasks(&queue, &[], &[], &["crates/ralph".to_string()], None);
126 assert_eq!(results.len(), 1);
127 assert_eq!(results[0].id, "RQ-0001");
128 }
129
130 #[test]
131 fn filter_tasks_with_multiple_scopes_or_logic() {
132 let queue = QueueFile {
133 version: 1,
134 tasks: vec![
135 task_with_scope("RQ-0001", vec!["crates/ralph".to_string()]),
136 task_with_scope("RQ-0002", vec!["docs".to_string()]),
137 task_with_scope("RQ-0003", vec!["crates/auth".to_string()]),
138 ],
139 };
140
141 let results = filter_tasks(
142 &queue,
143 &[],
144 &[],
145 &["crates/ralph".to_string(), "docs".to_string()],
146 None,
147 );
148 assert_eq!(results.len(), 2);
149 assert!(results.iter().any(|t| t.id == "RQ-0001"));
150 assert!(results.iter().any(|t| t.id == "RQ-0002"));
151 }
152
153 #[test]
154 fn filter_tasks_with_no_scope_filter() {
155 let queue = QueueFile {
156 version: 1,
157 tasks: vec![
158 task_with_scope("RQ-0001", vec!["crates/ralph".to_string()]),
159 task_with_scope("RQ-0002", vec!["docs/cli".to_string()]),
160 ],
161 };
162
163 let results = filter_tasks(&queue, &[], &[], &[], None);
164 assert_eq!(results.len(), 2);
165 }
166
167 #[test]
168 fn filter_tasks_combined_filters() {
169 let queue = QueueFile {
170 version: 1,
171 tasks: vec![
172 task_with_tags_scope_status(
173 "RQ-0001",
174 vec!["rust".to_string()],
175 vec!["crates/ralph".to_string()],
176 TaskStatus::Todo,
177 ),
178 task_with_tags_scope_status(
179 "RQ-0002",
180 vec!["docs".to_string()],
181 vec!["docs".to_string()],
182 TaskStatus::Done,
183 ),
184 task_with_tags_scope_status(
185 "RQ-0003",
186 vec!["rust".to_string()],
187 vec!["crates".to_string()],
188 TaskStatus::Doing,
189 ),
190 task_with_tags_scope_status(
191 "RQ-0004",
192 vec!["rust".to_string()],
193 vec!["crates/ralph".to_string()],
194 TaskStatus::Todo,
195 ),
196 ],
197 };
198
199 let results = filter_tasks(
200 &queue,
201 &[TaskStatus::Todo],
202 &["rust".to_string()],
203 &["crates/ralph".to_string()],
204 None,
205 );
206 assert_eq!(results.len(), 2);
207 assert!(results.iter().any(|t| t.id == "RQ-0001"));
208 assert!(results.iter().any(|t| t.id == "RQ-0004"));
209 }
210
211 #[test]
212 fn filter_tasks_status_only() {
213 let queue = QueueFile {
214 version: 1,
215 tasks: vec![
216 task_with_tags_scope_status("RQ-0001", vec![], vec![], TaskStatus::Todo),
217 task_with_tags_scope_status("RQ-0002", vec![], vec![], TaskStatus::Doing),
218 task_with_tags_scope_status("RQ-0003", vec![], vec![], TaskStatus::Todo),
219 ],
220 };
221
222 let results = filter_tasks(&queue, &[TaskStatus::Todo], &[], &[], None);
223 assert_eq!(results.len(), 2);
224 assert!(results.iter().all(|t| t.status == TaskStatus::Todo));
225 }
226
227 #[test]
228 fn filter_tasks_tag_only() {
229 let queue = QueueFile {
230 version: 1,
231 tasks: vec![
232 task_with_tags_scope_status(
233 "RQ-0001",
234 vec!["rust".to_string()],
235 vec![],
236 TaskStatus::Todo,
237 ),
238 task_with_tags_scope_status(
239 "RQ-0002",
240 vec!["docs".to_string()],
241 vec![],
242 TaskStatus::Todo,
243 ),
244 task_with_tags_scope_status(
245 "RQ-0003",
246 vec!["RUST".to_string()],
247 vec![],
248 TaskStatus::Doing,
249 ),
250 ],
251 };
252
253 let results = filter_tasks(&queue, &[], &["rust".to_string()], &[], None);
254 assert_eq!(results.len(), 2);
255 assert!(results.iter().any(|t| t.id == "RQ-0001"));
256 assert!(results.iter().any(|t| t.id == "RQ-0003"));
257 }
258
259 #[test]
260 fn filter_tasks_with_limit() {
261 let queue = QueueFile {
262 version: 1,
263 tasks: vec![
264 task_with_tags_scope_status(
265 "RQ-0001",
266 vec!["rust".to_string()],
267 vec!["crates/ralph".to_string()],
268 TaskStatus::Todo,
269 ),
270 task_with_tags_scope_status(
271 "RQ-0002",
272 vec!["rust".to_string()],
273 vec!["crates/ralph".to_string()],
274 TaskStatus::Todo,
275 ),
276 task_with_tags_scope_status(
277 "RQ-0003",
278 vec!["rust".to_string()],
279 vec!["crates/ralph".to_string()],
280 TaskStatus::Todo,
281 ),
282 ],
283 };
284
285 let results = filter_tasks(
286 &queue,
287 &[TaskStatus::Todo],
288 &["rust".to_string()],
289 &["crates/ralph".to_string()],
290 Some(2),
291 );
292 assert_eq!(results.len(), 2);
293 }
294}