clean_dev_dirs/project/
project.rs

1//! Core project data structures and types.
2//!
3//! This module defines the fundamental data structures used to represent
4//! development projects and their build artifacts throughout the application.
5
6use std::{
7    fmt::{Display, Formatter, Result},
8    path::PathBuf,
9};
10
11/// Enumeration of supported development project types.
12///
13/// This enum distinguishes between different types of development projects
14/// that the tool can detect and clean. Each project type has its own
15/// characteristic files and build directories.
16#[derive(Clone, PartialEq, Debug)]
17pub enum ProjectType {
18    /// Rust project with Cargo.toml and target/ directory
19    ///
20    /// Rust projects are identified by the presence of both a `Cargo.toml`
21    /// file and a `target/` directory in the same location.
22    Rust,
23
24    /// Node.js project with package.json and `node_modules`/ directory
25    ///
26    /// Node.js projects are identified by the presence of both a `package.json`
27    /// file and a `node_modules`/ directory in the same location.
28    Node,
29
30    /// Python project with requirements.txt, setup.py, or pyproject.toml and cache directories
31    ///
32    /// Python projects are identified by the presence of Python configuration files
33    /// and various cache/build directories like `__pycache__`, `.pytest_cache`, etc.
34    Python,
35
36    /// Go project with `go.mod` and vendor/ directory
37    ///
38    /// Go projects are identified by the presence of both a `go.mod`
39    /// file and a `vendor/` directory in the same location.
40    Go,
41}
42
43/// Information about build artifacts that can be cleaned.
44///
45/// This struct contains metadata about the build directory or artifacts
46/// that are candidates for cleanup, including their location and total size.
47#[derive(Clone)]
48pub struct BuildArtifacts {
49    /// Path to the build directory (target/ or `node_modules`/)
50    ///
51    /// This is the directory that will be deleted during cleanup operations.
52    /// For Rust projects, this points to the `target/` directory.
53    /// For Node.js projects, this points to the `node_modules/` directory.
54    pub path: PathBuf,
55
56    /// Total size of the build directory in bytes
57    ///
58    /// This value is calculated by recursively summing the sizes of all files
59    /// within the build directory. It's used for filtering and reporting purposes.
60    pub size: u64,
61}
62
63/// Representation of a development project with cleanable build artifacts.
64///
65/// This struct encapsulates all information about a development project,
66/// including its type, location, build artifacts, and metadata extracted
67/// from project configuration files.
68#[derive(Clone)]
69pub struct Project {
70    /// Type of the project (Rust or Node.js)
71    pub kind: ProjectType,
72
73    /// The root directory of the project where the configuration file is located
74    ///
75    /// For Rust projects, this is the directory containing `Cargo.toml`.
76    /// For Node.js projects, this is the directory containing `package.json`.
77    pub root_path: PathBuf,
78
79    /// The build directory to be cleaned and its metadata
80    ///
81    /// Contains information about the `target/` or `node_modules/` directory
82    /// that is a candidate for cleanup, including its path and total size.
83    pub build_arts: BuildArtifacts,
84
85    /// Name of the project extracted from configuration files
86    ///
87    /// For Rust projects, this is extracted from the `name` field in `Cargo.toml`.
88    /// For Node.js projects, this is extracted from the `name` field in `package.json`.
89    /// May be `None` if the name cannot be determined or parsed.
90    pub name: Option<String>,
91}
92
93impl Project {
94    /// Create a new project instance.
95    ///
96    /// This constructor creates a new `Project` with the specified parameters.
97    /// It's typically used by the scanner when a valid development project
98    /// is detected in the file system.
99    ///
100    /// # Arguments
101    ///
102    /// * `kind` - The type of project (Rust or Node.js)
103    /// * `root_path` - Path to the project's root directory
104    /// * `build_arts` - Information about the build artifacts to be cleaned
105    /// * `name` - Optional project name extracted from configuration files
106    ///
107    /// # Returns
108    ///
109    /// A new `Project` instance with the specified parameters.
110    ///
111    /// # Examples
112    ///
113    /// ```no_run
114    /// # use std::path::PathBuf;
115    /// # use crate::project::{Project, ProjectType, BuildArtifacts};
116    /// let build_arts = BuildArtifacts {
117    ///     path: PathBuf::from("/path/to/project/target"),
118    ///     size: 1024,
119    /// };
120    ///
121    /// let project = Project::new(
122    ///     ProjectType::Rust,
123    ///     PathBuf::from("/path/to/project"),
124    ///     build_arts,
125    ///     Some("my-project".to_string()),
126    /// );
127    /// ```
128    #[must_use]
129    pub fn new(
130        kind: ProjectType,
131        root_path: PathBuf,
132        build_arts: BuildArtifacts,
133        name: Option<String>,
134    ) -> Self {
135        Self {
136            kind,
137            root_path,
138            build_arts,
139            name,
140        }
141    }
142}
143
144impl Display for Project {
145    /// Format the project for display with the appropriate emoji and name.
146    ///
147    /// This implementation provides a human-readable representation of the project
148    /// that includes:
149    /// - An emoji indicator based on the project type (🦀 for Rust, 📦 for Node.js, 🐍 for Python, 🐹 for Go)
150    /// - The project name if available, otherwise just the path
151    /// - The project's root path
152    ///
153    /// # Examples
154    ///
155    /// - `🦀 my-rust-project (/path/to/project)`
156    /// - `📦 my-node-app (/path/to/app)`
157    /// - `🐍 my-python-project (/path/to/project)`
158    /// - `🐹 my-go-project (/path/to/project)`
159    /// - `🦀 /path/to/unnamed/project` (when no name is available)
160    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
161        let icon = match self.kind {
162            ProjectType::Rust => "🦀",
163            ProjectType::Node => "📦",
164            ProjectType::Python => "🐍",
165            ProjectType::Go => "🐹",
166        };
167
168        if let Some(name) = &self.name {
169            write!(f, "{icon} {name} ({})", self.root_path.display())
170        } else {
171            write!(f, "{icon} {}", self.root_path.display())
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use std::path::PathBuf;
180
181    /// Helper function to create a test `BuildArtifacts`
182    fn create_test_build_artifacts(path: &str, size: u64) -> BuildArtifacts {
183        BuildArtifacts {
184            path: PathBuf::from(path),
185            size,
186        }
187    }
188
189    /// Helper function to create a test Project
190    fn create_test_project(
191        kind: ProjectType,
192        root_path: &str,
193        build_path: &str,
194        size: u64,
195        name: Option<String>,
196    ) -> Project {
197        Project::new(
198            kind,
199            PathBuf::from(root_path),
200            create_test_build_artifacts(build_path, size),
201            name,
202        )
203    }
204
205    #[test]
206    fn test_project_type_equality() {
207        assert_eq!(ProjectType::Rust, ProjectType::Rust);
208        assert_eq!(ProjectType::Node, ProjectType::Node);
209        assert_eq!(ProjectType::Python, ProjectType::Python);
210        assert_eq!(ProjectType::Go, ProjectType::Go);
211
212        assert_ne!(ProjectType::Rust, ProjectType::Node);
213        assert_ne!(ProjectType::Node, ProjectType::Python);
214        assert_ne!(ProjectType::Python, ProjectType::Go);
215    }
216
217    #[test]
218    fn test_build_artifacts_creation() {
219        let artifacts = create_test_build_artifacts("/path/to/target", 1024);
220
221        assert_eq!(artifacts.path, PathBuf::from("/path/to/target"));
222        assert_eq!(artifacts.size, 1024);
223    }
224
225    #[test]
226    fn test_project_new() {
227        let project = create_test_project(
228            ProjectType::Rust,
229            "/path/to/project",
230            "/path/to/project/target",
231            1024,
232            Some("test-project".to_string()),
233        );
234
235        assert_eq!(project.kind, ProjectType::Rust);
236        assert_eq!(project.root_path, PathBuf::from("/path/to/project"));
237        assert_eq!(
238            project.build_arts.path,
239            PathBuf::from("/path/to/project/target")
240        );
241        assert_eq!(project.build_arts.size, 1024);
242        assert_eq!(project.name, Some("test-project".to_string()));
243    }
244
245    #[test]
246    fn test_project_display_with_name() {
247        let rust_project = create_test_project(
248            ProjectType::Rust,
249            "/path/to/rust-project",
250            "/path/to/rust-project/target",
251            1024,
252            Some("my-rust-app".to_string()),
253        );
254
255        let expected = "🦀 my-rust-app (/path/to/rust-project)";
256        assert_eq!(format!("{rust_project}"), expected);
257
258        let node_project = create_test_project(
259            ProjectType::Node,
260            "/path/to/node-project",
261            "/path/to/node-project/node_modules",
262            2048,
263            Some("my-node-app".to_string()),
264        );
265
266        let expected = "📦 my-node-app (/path/to/node-project)";
267        assert_eq!(format!("{node_project}"), expected);
268
269        let python_project = create_test_project(
270            ProjectType::Python,
271            "/path/to/python-project",
272            "/path/to/python-project/__pycache__",
273            512,
274            Some("my-python-app".to_string()),
275        );
276
277        let expected = "🐍 my-python-app (/path/to/python-project)";
278        assert_eq!(format!("{python_project}"), expected);
279
280        let go_project = create_test_project(
281            ProjectType::Go,
282            "/path/to/go-project",
283            "/path/to/go-project/vendor",
284            4096,
285            Some("my-go-app".to_string()),
286        );
287
288        let expected = "🐹 my-go-app (/path/to/go-project)";
289        assert_eq!(format!("{go_project}"), expected);
290    }
291
292    #[test]
293    fn test_project_display_without_name() {
294        let rust_project = create_test_project(
295            ProjectType::Rust,
296            "/path/to/unnamed-project",
297            "/path/to/unnamed-project/target",
298            1024,
299            None,
300        );
301
302        let expected = "🦀 /path/to/unnamed-project";
303        assert_eq!(format!("{rust_project}"), expected);
304
305        let node_project = create_test_project(
306            ProjectType::Node,
307            "/some/other/path",
308            "/some/other/path/node_modules",
309            2048,
310            None,
311        );
312
313        let expected = "📦 /some/other/path";
314        assert_eq!(format!("{node_project}"), expected);
315    }
316
317    #[test]
318    fn test_project_clone() {
319        let original = create_test_project(
320            ProjectType::Rust,
321            "/original/path",
322            "/original/path/target",
323            1024,
324            Some("original-project".to_string()),
325        );
326
327        let cloned = original.clone();
328
329        assert_eq!(original.kind, cloned.kind);
330        assert_eq!(original.root_path, cloned.root_path);
331        assert_eq!(original.build_arts.path, cloned.build_arts.path);
332        assert_eq!(original.build_arts.size, cloned.build_arts.size);
333        assert_eq!(original.name, cloned.name);
334    }
335
336    #[test]
337    fn test_build_artifacts_clone() {
338        let original = create_test_build_artifacts("/test/path", 2048);
339        let cloned = original.clone();
340
341        assert_eq!(original.path, cloned.path);
342        assert_eq!(original.size, cloned.size);
343    }
344
345    #[test]
346    fn test_project_with_zero_size() {
347        let project = create_test_project(
348            ProjectType::Python,
349            "/empty/project",
350            "/empty/project/__pycache__",
351            0,
352            Some("empty-project".to_string()),
353        );
354
355        assert_eq!(project.build_arts.size, 0);
356        assert_eq!(format!("{project}"), "🐍 empty-project (/empty/project)");
357    }
358
359    #[test]
360    fn test_project_with_large_size() {
361        let large_size = u64::MAX;
362        let project = create_test_project(
363            ProjectType::Go,
364            "/large/project",
365            "/large/project/vendor",
366            large_size,
367            Some("huge-project".to_string()),
368        );
369
370        assert_eq!(project.build_arts.size, large_size);
371    }
372}