clean_dev_dirs/
filtering.rs

1//! Project filtering functionality.
2//!
3//! This module provides functions for filtering projects based on various criteria
4//! such as size and modification time.
5
6use anyhow::Result;
7use chrono::{DateTime, Local};
8use rayon::prelude::*;
9use std::fs;
10
11use crate::config::FilterOptions;
12use crate::project::Project;
13use crate::utils::parse_size;
14
15/// Filter projects based on size and modification time criteria.
16///
17/// This function applies parallel filtering to remove projects that don't meet
18/// the specified criteria:
19/// - Projects smaller than the minimum size threshold
20/// - Projects modified more recently than the specified number of days
21///
22/// # Arguments
23///
24/// * `projects` - Vector of projects to filter
25/// * `filter_opts` - Filtering options containing size and time criteria
26///
27/// # Returns
28///
29/// - `Ok(Vec<Project>)` - Filtered list of projects that meet all criteria
30/// - `Err(anyhow::Error)` - If size parsing fails, or file system errors occur
31///
32/// # Errors
33///
34/// This function can return errors if:
35/// - The size string in `filter_opts.keep_size` cannot be parsed (invalid format)
36/// - Size value overflow occurs during parsing
37///
38/// # Examples
39///
40/// ```no_run
41/// # use clean_dev_dirs::{filtering::filter_projects, config::FilterOptions, project::Project};
42/// # use anyhow::Result;
43/// # fn example(projects: Vec<Project>) -> Result<()> {
44/// let filter_opts = FilterOptions {
45///     keep_size: "100MB".to_string(),
46///     keep_days: 30,
47/// };
48/// let filtered = filter_projects(projects, &filter_opts)?;
49/// # Ok(())
50/// # }
51/// ```
52pub fn filter_projects(
53    projects: Vec<Project>,
54    filter_opts: &FilterOptions,
55) -> Result<Vec<Project>> {
56    let keep_size_bytes = parse_size(&filter_opts.keep_size)?;
57    let keep_days = filter_opts.keep_days;
58
59    Ok(projects
60        .into_par_iter()
61        .filter(|project| meets_size_criteria(project, keep_size_bytes))
62        .filter(|project| meets_time_criteria(project, keep_days))
63        .collect())
64}
65
66/// Check if a project meets the size criteria.
67fn meets_size_criteria(project: &Project, min_size: u64) -> bool {
68    project.build_arts.size >= min_size
69}
70
71/// Check if a project meets the time criteria.
72fn meets_time_criteria(project: &Project, keep_days: u32) -> bool {
73    if keep_days == 0 {
74        return true;
75    }
76
77    is_project_old_enough(project, keep_days)
78}
79
80/// Check if a project is old enough based on its modification time.
81fn is_project_old_enough(project: &Project, keep_days: u32) -> bool {
82    let Result::Ok(metadata) = fs::metadata(&project.build_arts.path) else {
83        return true; // If we can't read metadata, don't filter it out
84    };
85
86    let Result::Ok(modified) = metadata.modified() else {
87        return true; // If we can't read modification time, don't filter it out
88    };
89
90    let modified_time: DateTime<Local> = modified.into();
91    let cutoff_time = Local::now() - chrono::Duration::days(i64::from(keep_days));
92
93    modified_time <= cutoff_time
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::project::{BuildArtifacts, Project, ProjectType};
100    use std::path::PathBuf;
101
102    /// Helper function to create a test project
103    fn create_test_project(
104        kind: ProjectType,
105        root_path: &str,
106        build_path: &str,
107        size: u64,
108        name: Option<String>,
109    ) -> Project {
110        Project::new(
111            kind,
112            PathBuf::from(root_path),
113            BuildArtifacts {
114                path: PathBuf::from(build_path),
115                size,
116            },
117            name,
118        )
119    }
120
121    #[test]
122    fn test_meets_size_criteria() {
123        let project = create_test_project(
124            ProjectType::Rust,
125            "/test",
126            "/test/target",
127            1_000_000, // 1MB
128            Some("test".to_string()),
129        );
130
131        assert!(meets_size_criteria(&project, 500_000)); // 0.5MB - should pass
132        assert!(meets_size_criteria(&project, 1_000_000)); // Exactly 1MB - should pass
133        assert!(!meets_size_criteria(&project, 2_000_000)); // 2MB - should fail
134    }
135
136    #[test]
137    fn test_meets_time_criteria_disabled() {
138        let project = create_test_project(
139            ProjectType::Rust,
140            "/test",
141            "/test/target",
142            1_000_000,
143            Some("test".to_string()),
144        );
145
146        // When keep_days is 0, should always return true
147        assert!(meets_time_criteria(&project, 0));
148    }
149}