1use anyhow::Result;
7use chrono::{DateTime, Local};
8use rayon::prelude::*;
9use std::fs;
10use std::time::SystemTime;
11
12use crate::config::filter::SortCriteria;
13use crate::config::{FilterOptions, SortOptions};
14use crate::project::{Project, ProjectType};
15use crate::utils::parse_size;
16
17pub fn filter_projects(
55 projects: Vec<Project>,
56 filter_opts: &FilterOptions,
57) -> Result<Vec<Project>> {
58 let keep_size_bytes = parse_size(&filter_opts.keep_size)?;
59 let keep_days = filter_opts.keep_days;
60
61 Ok(projects
62 .into_par_iter()
63 .filter(|project| meets_size_criteria(project, keep_size_bytes))
64 .filter(|project| meets_time_criteria(project, keep_days))
65 .collect())
66}
67
68const fn meets_size_criteria(project: &Project, min_size: u64) -> bool {
70 project.build_arts.size >= min_size
71}
72
73fn meets_time_criteria(project: &Project, keep_days: u32) -> bool {
75 if keep_days == 0 {
76 return true;
77 }
78
79 is_project_old_enough(project, keep_days)
80}
81
82fn is_project_old_enough(project: &Project, keep_days: u32) -> bool {
84 let Result::Ok(metadata) = fs::metadata(&project.build_arts.path) else {
85 return true; };
87
88 let Result::Ok(modified) = metadata.modified() else {
89 return true; };
91
92 let modified_time: DateTime<Local> = modified.into();
93 let cutoff_time = Local::now() - chrono::Duration::days(i64::from(keep_days));
94
95 modified_time <= cutoff_time
96}
97
98pub fn sort_projects(projects: &mut Vec<Project>, sort_opts: &SortOptions) {
131 let Some(criteria) = sort_opts.criteria else {
132 return;
133 };
134
135 match criteria {
136 SortCriteria::Size => {
137 projects.sort_by(|a, b| b.build_arts.size.cmp(&a.build_arts.size));
138 }
139 SortCriteria::Age => {
140 sort_by_age(projects);
141 }
142 SortCriteria::Name => {
143 projects.sort_by(|a, b| {
144 let name_a = a.name.as_deref().unwrap_or("");
145 let name_b = b.name.as_deref().unwrap_or("");
146 name_a.to_lowercase().cmp(&name_b.to_lowercase())
147 });
148 }
149 SortCriteria::Type => {
150 projects.sort_by(|a, b| type_order(&a.kind).cmp(&type_order(&b.kind)));
151 }
152 }
153
154 if sort_opts.reverse {
155 projects.reverse();
156 }
157}
158
159fn sort_by_age(projects: &mut Vec<Project>) {
164 let mut decorated: Vec<(Project, SystemTime)> = projects
165 .drain(..)
166 .map(|p| {
167 let mtime = fs::metadata(&p.build_arts.path)
168 .and_then(|m| m.modified())
169 .unwrap_or(SystemTime::UNIX_EPOCH);
170 (p, mtime)
171 })
172 .collect();
173
174 decorated.sort_by(|a, b| a.1.cmp(&b.1));
175
176 projects.extend(decorated.into_iter().map(|(p, _)| p));
177}
178
179const fn type_order(kind: &ProjectType) -> u8 {
184 match kind {
185 ProjectType::Cpp => 0,
186 ProjectType::DotNet => 1,
187 ProjectType::Go => 2,
188 ProjectType::Java => 3,
189 ProjectType::Node => 4,
190 ProjectType::Python => 5,
191 ProjectType::Rust => 6,
192 ProjectType::Swift => 7,
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199 use crate::project::{BuildArtifacts, Project, ProjectType};
200 use std::path::PathBuf;
201
202 fn create_test_project(
204 kind: ProjectType,
205 root_path: &str,
206 build_path: &str,
207 size: u64,
208 name: Option<String>,
209 ) -> Project {
210 Project::new(
211 kind,
212 PathBuf::from(root_path),
213 BuildArtifacts {
214 path: PathBuf::from(build_path),
215 size,
216 },
217 name,
218 )
219 }
220
221 #[test]
222 fn test_meets_size_criteria() {
223 let project = create_test_project(
224 ProjectType::Rust,
225 "/test",
226 "/test/target",
227 1_000_000, Some("test".to_string()),
229 );
230
231 assert!(meets_size_criteria(&project, 500_000)); assert!(meets_size_criteria(&project, 1_000_000)); assert!(!meets_size_criteria(&project, 2_000_000)); }
235
236 #[test]
237 fn test_meets_time_criteria_disabled() {
238 let project = create_test_project(
239 ProjectType::Rust,
240 "/test",
241 "/test/target",
242 1_000_000,
243 Some("test".to_string()),
244 );
245
246 assert!(meets_time_criteria(&project, 0));
248 }
249
250 #[test]
253 fn test_sort_by_size_descending() {
254 let mut projects = vec![
255 create_test_project(
256 ProjectType::Rust,
257 "/a",
258 "/a/target",
259 100,
260 Some("small".into()),
261 ),
262 create_test_project(
263 ProjectType::Rust,
264 "/b",
265 "/b/target",
266 300,
267 Some("large".into()),
268 ),
269 create_test_project(
270 ProjectType::Rust,
271 "/c",
272 "/c/target",
273 200,
274 Some("medium".into()),
275 ),
276 ];
277
278 let sort_opts = SortOptions {
279 criteria: Some(SortCriteria::Size),
280 reverse: false,
281 };
282 sort_projects(&mut projects, &sort_opts);
283
284 assert_eq!(projects[0].build_arts.size, 300);
285 assert_eq!(projects[1].build_arts.size, 200);
286 assert_eq!(projects[2].build_arts.size, 100);
287 }
288
289 #[test]
290 fn test_sort_by_size_reversed() {
291 let mut projects = vec![
292 create_test_project(
293 ProjectType::Rust,
294 "/a",
295 "/a/target",
296 100,
297 Some("small".into()),
298 ),
299 create_test_project(
300 ProjectType::Rust,
301 "/b",
302 "/b/target",
303 300,
304 Some("large".into()),
305 ),
306 create_test_project(
307 ProjectType::Rust,
308 "/c",
309 "/c/target",
310 200,
311 Some("medium".into()),
312 ),
313 ];
314
315 let sort_opts = SortOptions {
316 criteria: Some(SortCriteria::Size),
317 reverse: true,
318 };
319 sort_projects(&mut projects, &sort_opts);
320
321 assert_eq!(projects[0].build_arts.size, 100);
322 assert_eq!(projects[1].build_arts.size, 200);
323 assert_eq!(projects[2].build_arts.size, 300);
324 }
325
326 #[test]
327 fn test_sort_by_name_alphabetical() {
328 let mut projects = vec![
329 create_test_project(
330 ProjectType::Rust,
331 "/c",
332 "/c/target",
333 100,
334 Some("charlie".into()),
335 ),
336 create_test_project(
337 ProjectType::Rust,
338 "/a",
339 "/a/target",
340 100,
341 Some("alpha".into()),
342 ),
343 create_test_project(
344 ProjectType::Rust,
345 "/b",
346 "/b/target",
347 100,
348 Some("bravo".into()),
349 ),
350 ];
351
352 let sort_opts = SortOptions {
353 criteria: Some(SortCriteria::Name),
354 reverse: false,
355 };
356 sort_projects(&mut projects, &sort_opts);
357
358 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
359 assert_eq!(projects[1].name.as_deref(), Some("bravo"));
360 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
361 }
362
363 #[test]
364 fn test_sort_by_name_case_insensitive() {
365 let mut projects = vec![
366 create_test_project(
367 ProjectType::Rust,
368 "/c",
369 "/c/target",
370 100,
371 Some("Charlie".into()),
372 ),
373 create_test_project(
374 ProjectType::Rust,
375 "/a",
376 "/a/target",
377 100,
378 Some("alpha".into()),
379 ),
380 create_test_project(
381 ProjectType::Rust,
382 "/b",
383 "/b/target",
384 100,
385 Some("Bravo".into()),
386 ),
387 ];
388
389 let sort_opts = SortOptions {
390 criteria: Some(SortCriteria::Name),
391 reverse: false,
392 };
393 sort_projects(&mut projects, &sort_opts);
394
395 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
396 assert_eq!(projects[1].name.as_deref(), Some("Bravo"));
397 assert_eq!(projects[2].name.as_deref(), Some("Charlie"));
398 }
399
400 #[test]
401 fn test_sort_by_name_none_names_first() {
402 let mut projects = vec![
403 create_test_project(
404 ProjectType::Rust,
405 "/c",
406 "/c/target",
407 100,
408 Some("charlie".into()),
409 ),
410 create_test_project(ProjectType::Rust, "/a", "/a/target", 100, None),
411 create_test_project(
412 ProjectType::Rust,
413 "/b",
414 "/b/target",
415 100,
416 Some("alpha".into()),
417 ),
418 ];
419
420 let sort_opts = SortOptions {
421 criteria: Some(SortCriteria::Name),
422 reverse: false,
423 };
424 sort_projects(&mut projects, &sort_opts);
425
426 assert_eq!(projects[0].name.as_deref(), None);
428 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
429 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
430 }
431
432 #[test]
433 fn test_sort_by_type() {
434 let mut projects = vec![
435 create_test_project(
436 ProjectType::Rust,
437 "/r",
438 "/r/target",
439 100,
440 Some("rust-proj".into()),
441 ),
442 create_test_project(
443 ProjectType::Go,
444 "/g",
445 "/g/vendor",
446 100,
447 Some("go-proj".into()),
448 ),
449 create_test_project(
450 ProjectType::Python,
451 "/p",
452 "/p/__pycache__",
453 100,
454 Some("py-proj".into()),
455 ),
456 create_test_project(
457 ProjectType::Node,
458 "/n",
459 "/n/node_modules",
460 100,
461 Some("node-proj".into()),
462 ),
463 create_test_project(
464 ProjectType::Java,
465 "/j",
466 "/j/target",
467 100,
468 Some("java-proj".into()),
469 ),
470 create_test_project(
471 ProjectType::Cpp,
472 "/c",
473 "/c/build",
474 100,
475 Some("cpp-proj".into()),
476 ),
477 create_test_project(
478 ProjectType::Swift,
479 "/s",
480 "/s/.build",
481 100,
482 Some("swift-proj".into()),
483 ),
484 create_test_project(
485 ProjectType::DotNet,
486 "/d",
487 "/d/obj",
488 100,
489 Some("dotnet-proj".into()),
490 ),
491 ];
492
493 let sort_opts = SortOptions {
494 criteria: Some(SortCriteria::Type),
495 reverse: false,
496 };
497 sort_projects(&mut projects, &sort_opts);
498
499 assert_eq!(projects[0].kind, ProjectType::Cpp);
500 assert_eq!(projects[1].kind, ProjectType::DotNet);
501 assert_eq!(projects[2].kind, ProjectType::Go);
502 assert_eq!(projects[3].kind, ProjectType::Java);
503 assert_eq!(projects[4].kind, ProjectType::Node);
504 assert_eq!(projects[5].kind, ProjectType::Python);
505 assert_eq!(projects[6].kind, ProjectType::Rust);
506 assert_eq!(projects[7].kind, ProjectType::Swift);
507 }
508
509 #[test]
510 fn test_sort_by_type_reversed() {
511 let mut projects = vec![
512 create_test_project(
513 ProjectType::Go,
514 "/g",
515 "/g/vendor",
516 100,
517 Some("go-proj".into()),
518 ),
519 create_test_project(
520 ProjectType::Rust,
521 "/r",
522 "/r/target",
523 100,
524 Some("rust-proj".into()),
525 ),
526 create_test_project(
527 ProjectType::Node,
528 "/n",
529 "/n/node_modules",
530 100,
531 Some("node-proj".into()),
532 ),
533 ];
534
535 let sort_opts = SortOptions {
536 criteria: Some(SortCriteria::Type),
537 reverse: true,
538 };
539 sort_projects(&mut projects, &sort_opts);
540
541 assert_eq!(projects[0].kind, ProjectType::Rust);
542 assert_eq!(projects[1].kind, ProjectType::Node);
543 assert_eq!(projects[2].kind, ProjectType::Go);
544 }
545
546 #[test]
547 fn test_sort_none_criteria_preserves_order() {
548 let mut projects = vec![
549 create_test_project(
550 ProjectType::Rust,
551 "/c",
552 "/c/target",
553 100,
554 Some("charlie".into()),
555 ),
556 create_test_project(
557 ProjectType::Rust,
558 "/a",
559 "/a/target",
560 300,
561 Some("alpha".into()),
562 ),
563 create_test_project(
564 ProjectType::Rust,
565 "/b",
566 "/b/target",
567 200,
568 Some("bravo".into()),
569 ),
570 ];
571
572 let sort_opts = SortOptions {
573 criteria: None,
574 reverse: false,
575 };
576 sort_projects(&mut projects, &sort_opts);
577
578 assert_eq!(projects[0].name.as_deref(), Some("charlie"));
580 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
581 assert_eq!(projects[2].name.as_deref(), Some("bravo"));
582 }
583
584 #[test]
585 fn test_sort_empty_list() {
586 let mut projects: Vec<Project> = vec![];
587
588 let sort_opts = SortOptions {
589 criteria: Some(SortCriteria::Size),
590 reverse: false,
591 };
592 sort_projects(&mut projects, &sort_opts);
593
594 assert!(projects.is_empty());
595 }
596
597 #[test]
598 fn test_sort_single_element() {
599 let mut projects = vec![create_test_project(
600 ProjectType::Rust,
601 "/a",
602 "/a/target",
603 100,
604 Some("only".into()),
605 )];
606
607 let sort_opts = SortOptions {
608 criteria: Some(SortCriteria::Name),
609 reverse: false,
610 };
611 sort_projects(&mut projects, &sort_opts);
612
613 assert_eq!(projects.len(), 1);
614 assert_eq!(projects[0].name.as_deref(), Some("only"));
615 }
616
617 #[test]
618 fn test_type_order_values() {
619 assert!(type_order(&ProjectType::Cpp) < type_order(&ProjectType::DotNet));
620 assert!(type_order(&ProjectType::DotNet) < type_order(&ProjectType::Go));
621 assert!(type_order(&ProjectType::Go) < type_order(&ProjectType::Java));
622 assert!(type_order(&ProjectType::Java) < type_order(&ProjectType::Node));
623 assert!(type_order(&ProjectType::Node) < type_order(&ProjectType::Python));
624 assert!(type_order(&ProjectType::Python) < type_order(&ProjectType::Rust));
625 assert!(type_order(&ProjectType::Rust) < type_order(&ProjectType::Swift));
626 }
627}