Skip to main content

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
11use serde::Serialize;
12
13/// Enumeration of supported development project types.
14///
15/// This enum distinguishes between different types of development projects
16/// that the tool can detect and clean. Each project type has its own
17/// characteristic files and build directories.
18#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ProjectType {
21    /// Rust project with Cargo.toml and target/ directory
22    ///
23    /// Rust projects are identified by the presence of both a `Cargo.toml`
24    /// file and a `target/` directory in the same location.
25    Rust,
26
27    /// Node.js project with package.json and `node_modules`/ directory
28    ///
29    /// Node.js projects are identified by the presence of both a `package.json`
30    /// file and a `node_modules`/ directory in the same location.
31    Node,
32
33    /// Python project with requirements.txt, setup.py, or pyproject.toml and cache directories
34    ///
35    /// Python projects are identified by the presence of Python configuration files
36    /// and various cache/build directories like `__pycache__`, `.pytest_cache`, etc.
37    Python,
38
39    /// Go project with `go.mod` and vendor/ directory
40    ///
41    /// Go projects are identified by the presence of both a `go.mod`
42    /// file and a `vendor/` directory in the same location.
43    Go,
44
45    /// Java/Kotlin project with pom.xml or build.gradle and target/ or build/ directory
46    ///
47    /// Java/Kotlin projects are identified by the presence of Maven (`pom.xml`)
48    /// or Gradle (`build.gradle`, `build.gradle.kts`) configuration files along
49    /// with their respective build output directories.
50    Java,
51
52    /// C/C++ project with CMakeLists.txt or Makefile and build/ directory
53    ///
54    /// C/C++ projects are identified by the presence of build system files
55    /// (`CMakeLists.txt` or `Makefile`) alongside a `build/` directory.
56    Cpp,
57
58    /// Swift project with Package.swift and .build/ directory
59    ///
60    /// Swift Package Manager projects are identified by the presence of a
61    /// `Package.swift` manifest and the `.build/` directory.
62    Swift,
63
64    /// .NET/C# project with .csproj and bin/ + obj/ directories
65    ///
66    /// .NET projects are identified by the presence of `.csproj` project files
67    /// alongside `bin/` and/or `obj/` output directories.
68    DotNet,
69
70    /// Ruby project with Gemfile and .bundle/ or vendor/bundle/ directory
71    ///
72    /// Ruby projects are identified by the presence of a `Gemfile`
73    /// alongside a `.bundle/` or `vendor/bundle/` directory.
74    Ruby,
75
76    /// Elixir project with mix.exs and _build/ directory
77    ///
78    /// Elixir projects are identified by the presence of a `mix.exs`
79    /// file and a `_build/` directory.
80    Elixir,
81
82    /// Deno project with deno.json or deno.jsonc and vendor/ or `node_modules`/ directory
83    ///
84    /// Deno projects are identified by the presence of a `deno.json` or `deno.jsonc`
85    /// file alongside a `vendor/` directory (from `deno vendor`) or a `node_modules/`
86    /// directory (Deno 2 npm support without a `package.json`).
87    Deno,
88}
89
90/// Information about build artifacts that can be cleaned.
91///
92/// This struct contains metadata about the build directory or artifacts
93/// that are candidates for cleanup, including their location and total size.
94#[derive(Clone, Serialize)]
95pub struct BuildArtifacts {
96    /// Path to the build directory (target/ or `node_modules`/)
97    ///
98    /// This is the directory that will be deleted during cleanup operations.
99    /// For Rust projects, this points to the `target/` directory.
100    /// For Node.js projects, this points to the `node_modules/` directory.
101    pub path: PathBuf,
102
103    /// Total size of the build directory in bytes
104    ///
105    /// This value is calculated by recursively summing the sizes of all files
106    /// within the build directory. It's used for filtering and reporting purposes.
107    pub size: u64,
108}
109
110/// Representation of a development project with cleanable build artifacts.
111///
112/// This struct encapsulates all information about a development project,
113/// including its type, location, build artifacts, and metadata extracted
114/// from project configuration files.
115#[derive(Clone, Serialize)]
116pub struct Project {
117    /// Type of the project (Rust or Node.js)
118    pub kind: ProjectType,
119
120    /// The root directory of the project where the configuration file is located
121    ///
122    /// For Rust projects, this is the directory containing `Cargo.toml`.
123    /// For Node.js projects, this is the directory containing `package.json`.
124    pub root_path: PathBuf,
125
126    /// The build directory to be cleaned and its metadata
127    ///
128    /// Contains information about the `target/` or `node_modules/` directory
129    /// that is a candidate for cleanup, including its path and total size.
130    pub build_arts: BuildArtifacts,
131
132    /// Name of the project extracted from configuration files
133    ///
134    /// For Rust projects, this is extracted from the `name` field in `Cargo.toml`.
135    /// For Node.js projects, this is extracted from the `name` field in `package.json`.
136    /// May be `None` if the name cannot be determined or parsed.
137    pub name: Option<String>,
138}
139
140impl Project {
141    /// Create a new project instance.
142    ///
143    /// This constructor creates a new `Project` with the specified parameters.
144    /// It's typically used by the scanner when a valid development project
145    /// is detected in the file system.
146    ///
147    /// # Arguments
148    ///
149    /// * `kind` - The type of project (Rust or Node.js)
150    /// * `root_path` - Path to the project's root directory
151    /// * `build_arts` - Information about the build artifacts to be cleaned
152    /// * `name` - Optional project name extracted from configuration files
153    ///
154    /// # Returns
155    ///
156    /// A new `Project` instance with the specified parameters.
157    ///
158    /// # Examples
159    ///
160    /// ```no_run
161    /// # use std::path::PathBuf;
162    /// # use crate::project::{Project, ProjectType, BuildArtifacts};
163    /// let build_arts = BuildArtifacts {
164    ///     path: PathBuf::from("/path/to/project/target"),
165    ///     size: 1024,
166    /// };
167    ///
168    /// let project = Project::new(
169    ///     ProjectType::Rust,
170    ///     PathBuf::from("/path/to/project"),
171    ///     build_arts,
172    ///     Some("my-project".to_string()),
173    /// );
174    /// ```
175    #[must_use]
176    pub const fn new(
177        kind: ProjectType,
178        root_path: PathBuf,
179        build_arts: BuildArtifacts,
180        name: Option<String>,
181    ) -> Self {
182        Self {
183            kind,
184            root_path,
185            build_arts,
186            name,
187        }
188    }
189}
190
191impl Display for Project {
192    /// Format the project for display with the appropriate emoji and name.
193    ///
194    /// This implementation provides a human-readable representation of the project
195    /// that includes:
196    /// - An emoji indicator based on the project type (🦀 for Rust, 📦 for Node.js, 🐍 for Python, 🐹 for Go)
197    /// - The project name if available, otherwise just the path
198    /// - The project's root path
199    ///
200    /// # Examples
201    ///
202    /// - `🦀 my-rust-project (/path/to/project)`
203    /// - `📦 my-node-app (/path/to/app)`
204    /// - `🐍 my-python-project (/path/to/project)`
205    /// - `🐹 my-go-project (/path/to/project)`
206    /// - `☕ my-java-project (/path/to/project)`
207    /// - `⚙️ my-cpp-project (/path/to/project)`
208    /// - `🐦 my-swift-project (/path/to/project)`
209    /// - `🔷 my-dotnet-project (/path/to/project)`
210    /// - `🦀 /path/to/unnamed/project` (when no name is available)
211    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
212        let icon = match self.kind {
213            ProjectType::Rust => "🦀",
214            ProjectType::Node => "📦",
215            ProjectType::Python => "🐍",
216            ProjectType::Go => "🐹",
217            ProjectType::Java => "☕",
218            ProjectType::Cpp => "⚙️",
219            ProjectType::Swift => "🐦",
220            ProjectType::DotNet => "🔷",
221            ProjectType::Ruby => "💎",
222            ProjectType::Elixir => "💧",
223            ProjectType::Deno => "🦕",
224        };
225
226        if let Some(name) = &self.name {
227            write!(f, "{icon} {name} ({})", self.root_path.display())
228        } else {
229            write!(f, "{icon} {}", self.root_path.display())
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use std::path::PathBuf;
238
239    /// Helper function to create a test `BuildArtifacts`
240    fn create_test_build_artifacts(path: &str, size: u64) -> BuildArtifacts {
241        BuildArtifacts {
242            path: PathBuf::from(path),
243            size,
244        }
245    }
246
247    /// Helper function to create a test Project
248    fn create_test_project(
249        kind: ProjectType,
250        root_path: &str,
251        build_path: &str,
252        size: u64,
253        name: Option<String>,
254    ) -> Project {
255        Project::new(
256            kind,
257            PathBuf::from(root_path),
258            create_test_build_artifacts(build_path, size),
259            name,
260        )
261    }
262
263    #[test]
264    fn test_project_type_equality() {
265        assert_eq!(ProjectType::Rust, ProjectType::Rust);
266        assert_eq!(ProjectType::Node, ProjectType::Node);
267        assert_eq!(ProjectType::Python, ProjectType::Python);
268        assert_eq!(ProjectType::Go, ProjectType::Go);
269        assert_eq!(ProjectType::Java, ProjectType::Java);
270        assert_eq!(ProjectType::Cpp, ProjectType::Cpp);
271        assert_eq!(ProjectType::Swift, ProjectType::Swift);
272        assert_eq!(ProjectType::DotNet, ProjectType::DotNet);
273        assert_eq!(ProjectType::Ruby, ProjectType::Ruby);
274        assert_eq!(ProjectType::Elixir, ProjectType::Elixir);
275        assert_eq!(ProjectType::Deno, ProjectType::Deno);
276
277        assert_ne!(ProjectType::Rust, ProjectType::Node);
278        assert_ne!(ProjectType::Node, ProjectType::Python);
279        assert_ne!(ProjectType::Python, ProjectType::Go);
280        assert_ne!(ProjectType::Go, ProjectType::Java);
281        assert_ne!(ProjectType::Java, ProjectType::Cpp);
282        assert_ne!(ProjectType::Cpp, ProjectType::Swift);
283        assert_ne!(ProjectType::Swift, ProjectType::DotNet);
284        assert_ne!(ProjectType::DotNet, ProjectType::Ruby);
285        assert_ne!(ProjectType::Ruby, ProjectType::Elixir);
286        assert_ne!(ProjectType::Elixir, ProjectType::Deno);
287    }
288
289    #[test]
290    fn test_build_artifacts_creation() {
291        let artifacts = create_test_build_artifacts("/path/to/target", 1024);
292
293        assert_eq!(artifacts.path, PathBuf::from("/path/to/target"));
294        assert_eq!(artifacts.size, 1024);
295    }
296
297    #[test]
298    fn test_project_new() {
299        let project = create_test_project(
300            ProjectType::Rust,
301            "/path/to/project",
302            "/path/to/project/target",
303            1024,
304            Some("test-project".to_string()),
305        );
306
307        assert_eq!(project.kind, ProjectType::Rust);
308        assert_eq!(project.root_path, PathBuf::from("/path/to/project"));
309        assert_eq!(
310            project.build_arts.path,
311            PathBuf::from("/path/to/project/target")
312        );
313        assert_eq!(project.build_arts.size, 1024);
314        assert_eq!(project.name, Some("test-project".to_string()));
315    }
316
317    #[test]
318    fn test_project_display_with_name() {
319        let rust_project = create_test_project(
320            ProjectType::Rust,
321            "/path/to/rust-project",
322            "/path/to/rust-project/target",
323            1024,
324            Some("my-rust-app".to_string()),
325        );
326
327        let expected = "🦀 my-rust-app (/path/to/rust-project)";
328        assert_eq!(format!("{rust_project}"), expected);
329
330        let node_project = create_test_project(
331            ProjectType::Node,
332            "/path/to/node-project",
333            "/path/to/node-project/node_modules",
334            2048,
335            Some("my-node-app".to_string()),
336        );
337
338        let expected = "📦 my-node-app (/path/to/node-project)";
339        assert_eq!(format!("{node_project}"), expected);
340
341        let python_project = create_test_project(
342            ProjectType::Python,
343            "/path/to/python-project",
344            "/path/to/python-project/__pycache__",
345            512,
346            Some("my-python-app".to_string()),
347        );
348
349        let expected = "🐍 my-python-app (/path/to/python-project)";
350        assert_eq!(format!("{python_project}"), expected);
351
352        let go_project = create_test_project(
353            ProjectType::Go,
354            "/path/to/go-project",
355            "/path/to/go-project/vendor",
356            4096,
357            Some("my-go-app".to_string()),
358        );
359
360        let expected = "🐹 my-go-app (/path/to/go-project)";
361        assert_eq!(format!("{go_project}"), expected);
362
363        let java_project = create_test_project(
364            ProjectType::Java,
365            "/path/to/java-project",
366            "/path/to/java-project/target",
367            8192,
368            Some("my-java-app".to_string()),
369        );
370
371        let expected = "☕ my-java-app (/path/to/java-project)";
372        assert_eq!(format!("{java_project}"), expected);
373
374        let cpp_project = create_test_project(
375            ProjectType::Cpp,
376            "/path/to/cpp-project",
377            "/path/to/cpp-project/build",
378            2048,
379            Some("my-cpp-app".to_string()),
380        );
381
382        let expected = "⚙\u{fe0f} my-cpp-app (/path/to/cpp-project)";
383        assert_eq!(format!("{cpp_project}"), expected);
384
385        let swift_project = create_test_project(
386            ProjectType::Swift,
387            "/path/to/swift-project",
388            "/path/to/swift-project/.build",
389            1024,
390            Some("my-swift-app".to_string()),
391        );
392
393        let expected = "🐦 my-swift-app (/path/to/swift-project)";
394        assert_eq!(format!("{swift_project}"), expected);
395
396        let dotnet_project = create_test_project(
397            ProjectType::DotNet,
398            "/path/to/dotnet-project",
399            "/path/to/dotnet-project/obj",
400            4096,
401            Some("my-dotnet-app".to_string()),
402        );
403
404        let expected = "🔷 my-dotnet-app (/path/to/dotnet-project)";
405        assert_eq!(format!("{dotnet_project}"), expected);
406
407        let ruby_project = create_test_project(
408            ProjectType::Ruby,
409            "/path/to/ruby-project",
410            "/path/to/ruby-project/vendor/bundle",
411            2048,
412            Some("my-ruby-gem".to_string()),
413        );
414
415        let expected = "💎 my-ruby-gem (/path/to/ruby-project)";
416        assert_eq!(format!("{ruby_project}"), expected);
417
418        let elixir_project = create_test_project(
419            ProjectType::Elixir,
420            "/path/to/elixir-project",
421            "/path/to/elixir-project/_build",
422            1024,
423            Some("my_elixir_app".to_string()),
424        );
425
426        let expected = "💧 my_elixir_app (/path/to/elixir-project)";
427        assert_eq!(format!("{elixir_project}"), expected);
428
429        let deno_project = create_test_project(
430            ProjectType::Deno,
431            "/path/to/deno-project",
432            "/path/to/deno-project/vendor",
433            512,
434            Some("my-deno-app".to_string()),
435        );
436
437        let expected = "🦕 my-deno-app (/path/to/deno-project)";
438        assert_eq!(format!("{deno_project}"), expected);
439    }
440
441    #[test]
442    fn test_project_display_without_name() {
443        let rust_project = create_test_project(
444            ProjectType::Rust,
445            "/path/to/unnamed-project",
446            "/path/to/unnamed-project/target",
447            1024,
448            None,
449        );
450
451        let expected = "🦀 /path/to/unnamed-project";
452        assert_eq!(format!("{rust_project}"), expected);
453
454        let node_project = create_test_project(
455            ProjectType::Node,
456            "/some/other/path",
457            "/some/other/path/node_modules",
458            2048,
459            None,
460        );
461
462        let expected = "📦 /some/other/path";
463        assert_eq!(format!("{node_project}"), expected);
464    }
465
466    #[test]
467    fn test_project_clone() {
468        let original = create_test_project(
469            ProjectType::Rust,
470            "/original/path",
471            "/original/path/target",
472            1024,
473            Some("original-project".to_string()),
474        );
475
476        let cloned = original.clone();
477
478        assert_eq!(original.kind, cloned.kind);
479        assert_eq!(original.root_path, cloned.root_path);
480        assert_eq!(original.build_arts.path, cloned.build_arts.path);
481        assert_eq!(original.build_arts.size, cloned.build_arts.size);
482        assert_eq!(original.name, cloned.name);
483    }
484
485    #[test]
486    fn test_build_artifacts_clone() {
487        let original = create_test_build_artifacts("/test/path", 2048);
488        let cloned = original.clone();
489
490        assert_eq!(original.path, cloned.path);
491        assert_eq!(original.size, cloned.size);
492    }
493
494    #[test]
495    fn test_project_with_zero_size() {
496        let project = create_test_project(
497            ProjectType::Python,
498            "/empty/project",
499            "/empty/project/__pycache__",
500            0,
501            Some("empty-project".to_string()),
502        );
503
504        assert_eq!(project.build_arts.size, 0);
505        assert_eq!(format!("{project}"), "🐍 empty-project (/empty/project)");
506    }
507
508    #[test]
509    fn test_project_with_large_size() {
510        let large_size = u64::MAX;
511        let project = create_test_project(
512            ProjectType::Go,
513            "/large/project",
514            "/large/project/vendor",
515            large_size,
516            Some("huge-project".to_string()),
517        );
518
519        assert_eq!(project.build_arts.size, large_size);
520    }
521}