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    /// PHP project with composer.json and vendor/ directory
90    ///
91    /// PHP projects are identified by the presence of a `composer.json`
92    /// file and a `vendor/` directory (Composer dependencies).
93    Php,
94
95    /// Haskell project with stack.yaml or cabal.project and .stack-work/ or dist-newstyle/
96    ///
97    /// Haskell projects are identified by either Stack (`stack.yaml` + `.stack-work/`)
98    /// or Cabal (`cabal.project` or `*.cabal` + `dist-newstyle/`).
99    Haskell,
100
101    /// Dart/Flutter project with pubspec.yaml and `.dart_tool`/ or build/
102    ///
103    /// Dart/Flutter projects are identified by the presence of a `pubspec.yaml`
104    /// file alongside a `.dart_tool/` directory and/or a `build/` directory.
105    Dart,
106
107    /// Zig project with build.zig and zig-cache/ or zig-out/
108    ///
109    /// Zig projects are identified by the presence of a `build.zig`
110    /// file alongside a `zig-cache/` or `zig-out/` directory.
111    Zig,
112
113    /// Scala project with build.sbt and target/ directory
114    ///
115    /// Scala projects are identified by the presence of a `build.sbt`
116    /// file and a `target/` directory.
117    Scala,
118}
119
120/// Information about build artifacts that can be cleaned.
121///
122/// This struct contains metadata about the build directory or artifacts
123/// that are candidates for cleanup, including their location and total size.
124#[derive(Clone, Serialize)]
125pub struct BuildArtifacts {
126    /// Path to the build directory (target/ or `node_modules`/)
127    ///
128    /// This is the directory that will be deleted during cleanup operations.
129    /// For Rust projects, this points to the `target/` directory.
130    /// For Node.js projects, this points to the `node_modules/` directory.
131    pub path: PathBuf,
132
133    /// Total size of the build directory in bytes
134    ///
135    /// This value is calculated by recursively summing the sizes of all files
136    /// within the build directory. It's used for filtering and reporting purposes.
137    pub size: u64,
138}
139
140/// Representation of a development project with cleanable build artifacts.
141///
142/// This struct encapsulates all information about a development project,
143/// including its type, location, build artifacts, and metadata extracted
144/// from project configuration files.
145#[derive(Clone, Serialize)]
146pub struct Project {
147    /// Type of the project (Rust or Node.js)
148    pub kind: ProjectType,
149
150    /// The root directory of the project where the configuration file is located
151    ///
152    /// For Rust projects, this is the directory containing `Cargo.toml`.
153    /// For Node.js projects, this is the directory containing `package.json`.
154    pub root_path: PathBuf,
155
156    /// The build directories to be cleaned and their metadata.
157    ///
158    /// Most project types have a single artifact directory, but some (e.g. Python,
159    /// .NET, Ruby) can produce several cleanable directories simultaneously.
160    pub build_arts: Vec<BuildArtifacts>,
161
162    /// Name of the project extracted from configuration files
163    ///
164    /// For Rust projects, this is extracted from the `name` field in `Cargo.toml`.
165    /// For Node.js projects, this is extracted from the `name` field in `package.json`.
166    /// May be `None` if the name cannot be determined or parsed.
167    pub name: Option<String>,
168}
169
170impl Project {
171    /// Create a new project instance.
172    ///
173    /// This constructor creates a new `Project` with the specified parameters.
174    /// It's typically used by the scanner when a valid development project
175    /// is detected in the file system.
176    ///
177    /// # Arguments
178    ///
179    /// * `kind` - The type of project (Rust or Node.js)
180    /// * `root_path` - Path to the project's root directory
181    /// * `build_arts` - Information about the build artifacts to be cleaned
182    /// * `name` - Optional project name extracted from configuration files
183    ///
184    /// # Returns
185    ///
186    /// A new `Project` instance with the specified parameters.
187    ///
188    /// # Examples
189    ///
190    /// ```no_run
191    /// # use std::path::PathBuf;
192    /// # use crate::project::{Project, ProjectType, BuildArtifacts};
193    /// let build_arts = vec![BuildArtifacts {
194    ///     path: PathBuf::from("/path/to/project/target"),
195    ///     size: 1024,
196    /// }];
197    ///
198    /// let project = Project::new(
199    ///     ProjectType::Rust,
200    ///     PathBuf::from("/path/to/project"),
201    ///     build_arts,
202    ///     Some("my-project".to_string()),
203    /// );
204    /// ```
205    #[must_use]
206    pub const fn new(
207        kind: ProjectType,
208        root_path: PathBuf,
209        build_arts: Vec<BuildArtifacts>,
210        name: Option<String>,
211    ) -> Self {
212        Self {
213            kind,
214            root_path,
215            build_arts,
216            name,
217        }
218    }
219
220    /// Return the sum of sizes across all build artifact directories.
221    #[must_use]
222    pub fn total_size(&self) -> u64 {
223        self.build_arts.iter().map(|a| a.size).sum()
224    }
225}
226
227impl Display for Project {
228    /// Format the project for display with the appropriate emoji and name.
229    ///
230    /// This implementation provides a human-readable representation of the project
231    /// that includes:
232    /// - An emoji indicator based on the project type (🦀 for Rust, 📦 for Node.js, 🐍 for Python, 🐹 for Go)
233    /// - The project name if available, otherwise just the path
234    /// - The project's root path
235    ///
236    /// # Examples
237    ///
238    /// - `🦀 my-rust-project (/path/to/project)`
239    /// - `📦 my-node-app (/path/to/app)`
240    /// - `🐍 my-python-project (/path/to/project)`
241    /// - `🐹 my-go-project (/path/to/project)`
242    /// - `☕ my-java-project (/path/to/project)`
243    /// - `⚙️ my-cpp-project (/path/to/project)`
244    /// - `🐦 my-swift-project (/path/to/project)`
245    /// - `🔷 my-dotnet-project (/path/to/project)`
246    /// - `🦀 /path/to/unnamed/project` (when no name is available)
247    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
248        let icon = match self.kind {
249            ProjectType::Rust => "🦀",
250            ProjectType::Node => "📦",
251            ProjectType::Python => "🐍",
252            ProjectType::Go => "🐹",
253            ProjectType::Java => "☕",
254            ProjectType::Cpp => "⚙️",
255            ProjectType::Swift => "🐦",
256            ProjectType::DotNet => "🔷",
257            ProjectType::Ruby => "💎",
258            ProjectType::Elixir => "💧",
259            ProjectType::Deno => "🦕",
260            ProjectType::Php => "🐘",
261            ProjectType::Haskell => "λ",
262            ProjectType::Dart => "🎯",
263            ProjectType::Zig => "⚡",
264            ProjectType::Scala => "🔴",
265        };
266
267        if let Some(name) = &self.name {
268            write!(f, "{icon} {name} ({})", self.root_path.display())
269        } else {
270            write!(f, "{icon} {}", self.root_path.display())
271        }
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use std::path::PathBuf;
279
280    /// Helper function to create a test `BuildArtifacts`
281    fn create_test_build_artifacts(path: &str, size: u64) -> BuildArtifacts {
282        BuildArtifacts {
283            path: PathBuf::from(path),
284            size,
285        }
286    }
287
288    /// Helper function to create a test Project
289    fn create_test_project(
290        kind: ProjectType,
291        root_path: &str,
292        build_path: &str,
293        size: u64,
294        name: Option<String>,
295    ) -> Project {
296        Project::new(
297            kind,
298            PathBuf::from(root_path),
299            vec![create_test_build_artifacts(build_path, size)],
300            name,
301        )
302    }
303
304    #[test]
305    fn test_project_type_equality() {
306        assert_eq!(ProjectType::Rust, ProjectType::Rust);
307        assert_eq!(ProjectType::Node, ProjectType::Node);
308        assert_eq!(ProjectType::Python, ProjectType::Python);
309        assert_eq!(ProjectType::Go, ProjectType::Go);
310        assert_eq!(ProjectType::Java, ProjectType::Java);
311        assert_eq!(ProjectType::Cpp, ProjectType::Cpp);
312        assert_eq!(ProjectType::Swift, ProjectType::Swift);
313        assert_eq!(ProjectType::DotNet, ProjectType::DotNet);
314        assert_eq!(ProjectType::Ruby, ProjectType::Ruby);
315        assert_eq!(ProjectType::Elixir, ProjectType::Elixir);
316        assert_eq!(ProjectType::Deno, ProjectType::Deno);
317        assert_eq!(ProjectType::Php, ProjectType::Php);
318        assert_eq!(ProjectType::Haskell, ProjectType::Haskell);
319        assert_eq!(ProjectType::Dart, ProjectType::Dart);
320        assert_eq!(ProjectType::Zig, ProjectType::Zig);
321        assert_eq!(ProjectType::Scala, ProjectType::Scala);
322
323        assert_ne!(ProjectType::Rust, ProjectType::Node);
324        assert_ne!(ProjectType::Node, ProjectType::Python);
325        assert_ne!(ProjectType::Python, ProjectType::Go);
326        assert_ne!(ProjectType::Go, ProjectType::Java);
327        assert_ne!(ProjectType::Java, ProjectType::Cpp);
328        assert_ne!(ProjectType::Cpp, ProjectType::Swift);
329        assert_ne!(ProjectType::Swift, ProjectType::DotNet);
330        assert_ne!(ProjectType::DotNet, ProjectType::Ruby);
331        assert_ne!(ProjectType::Ruby, ProjectType::Elixir);
332        assert_ne!(ProjectType::Elixir, ProjectType::Deno);
333        assert_ne!(ProjectType::Deno, ProjectType::Php);
334        assert_ne!(ProjectType::Php, ProjectType::Haskell);
335        assert_ne!(ProjectType::Haskell, ProjectType::Dart);
336        assert_ne!(ProjectType::Dart, ProjectType::Zig);
337        assert_ne!(ProjectType::Zig, ProjectType::Scala);
338    }
339
340    #[test]
341    fn test_build_artifacts_creation() {
342        let artifacts = create_test_build_artifacts("/path/to/target", 1024);
343
344        assert_eq!(artifacts.path, PathBuf::from("/path/to/target"));
345        assert_eq!(artifacts.size, 1024);
346    }
347
348    #[test]
349    fn test_project_new() {
350        let project = create_test_project(
351            ProjectType::Rust,
352            "/path/to/project",
353            "/path/to/project/target",
354            1024,
355            Some("test-project".to_string()),
356        );
357
358        assert_eq!(project.kind, ProjectType::Rust);
359        assert_eq!(project.root_path, PathBuf::from("/path/to/project"));
360        assert_eq!(
361            project.build_arts[0].path,
362            PathBuf::from("/path/to/project/target")
363        );
364        assert_eq!(project.build_arts[0].size, 1024);
365        assert_eq!(project.name, Some("test-project".to_string()));
366    }
367
368    #[test]
369    fn test_project_display_with_name() {
370        let rust_project = create_test_project(
371            ProjectType::Rust,
372            "/path/to/rust-project",
373            "/path/to/rust-project/target",
374            1024,
375            Some("my-rust-app".to_string()),
376        );
377
378        let expected = "🦀 my-rust-app (/path/to/rust-project)";
379        assert_eq!(format!("{rust_project}"), expected);
380
381        let node_project = create_test_project(
382            ProjectType::Node,
383            "/path/to/node-project",
384            "/path/to/node-project/node_modules",
385            2048,
386            Some("my-node-app".to_string()),
387        );
388
389        let expected = "📦 my-node-app (/path/to/node-project)";
390        assert_eq!(format!("{node_project}"), expected);
391
392        let python_project = create_test_project(
393            ProjectType::Python,
394            "/path/to/python-project",
395            "/path/to/python-project/__pycache__",
396            512,
397            Some("my-python-app".to_string()),
398        );
399
400        let expected = "🐍 my-python-app (/path/to/python-project)";
401        assert_eq!(format!("{python_project}"), expected);
402
403        let go_project = create_test_project(
404            ProjectType::Go,
405            "/path/to/go-project",
406            "/path/to/go-project/vendor",
407            4096,
408            Some("my-go-app".to_string()),
409        );
410
411        let expected = "🐹 my-go-app (/path/to/go-project)";
412        assert_eq!(format!("{go_project}"), expected);
413
414        let java_project = create_test_project(
415            ProjectType::Java,
416            "/path/to/java-project",
417            "/path/to/java-project/target",
418            8192,
419            Some("my-java-app".to_string()),
420        );
421
422        let expected = "☕ my-java-app (/path/to/java-project)";
423        assert_eq!(format!("{java_project}"), expected);
424
425        let cpp_project = create_test_project(
426            ProjectType::Cpp,
427            "/path/to/cpp-project",
428            "/path/to/cpp-project/build",
429            2048,
430            Some("my-cpp-app".to_string()),
431        );
432
433        let expected = "⚙\u{fe0f} my-cpp-app (/path/to/cpp-project)";
434        assert_eq!(format!("{cpp_project}"), expected);
435
436        let swift_project = create_test_project(
437            ProjectType::Swift,
438            "/path/to/swift-project",
439            "/path/to/swift-project/.build",
440            1024,
441            Some("my-swift-app".to_string()),
442        );
443
444        let expected = "🐦 my-swift-app (/path/to/swift-project)";
445        assert_eq!(format!("{swift_project}"), expected);
446
447        let dotnet_project = create_test_project(
448            ProjectType::DotNet,
449            "/path/to/dotnet-project",
450            "/path/to/dotnet-project/obj",
451            4096,
452            Some("my-dotnet-app".to_string()),
453        );
454
455        let expected = "🔷 my-dotnet-app (/path/to/dotnet-project)";
456        assert_eq!(format!("{dotnet_project}"), expected);
457
458        let ruby_project = create_test_project(
459            ProjectType::Ruby,
460            "/path/to/ruby-project",
461            "/path/to/ruby-project/vendor/bundle",
462            2048,
463            Some("my-ruby-gem".to_string()),
464        );
465
466        let expected = "💎 my-ruby-gem (/path/to/ruby-project)";
467        assert_eq!(format!("{ruby_project}"), expected);
468
469        let elixir_project = create_test_project(
470            ProjectType::Elixir,
471            "/path/to/elixir-project",
472            "/path/to/elixir-project/_build",
473            1024,
474            Some("my_elixir_app".to_string()),
475        );
476
477        let expected = "💧 my_elixir_app (/path/to/elixir-project)";
478        assert_eq!(format!("{elixir_project}"), expected);
479
480        let deno_project = create_test_project(
481            ProjectType::Deno,
482            "/path/to/deno-project",
483            "/path/to/deno-project/vendor",
484            512,
485            Some("my-deno-app".to_string()),
486        );
487
488        let expected = "🦕 my-deno-app (/path/to/deno-project)";
489        assert_eq!(format!("{deno_project}"), expected);
490    }
491
492    #[test]
493    fn test_project_display_without_name() {
494        let rust_project = create_test_project(
495            ProjectType::Rust,
496            "/path/to/unnamed-project",
497            "/path/to/unnamed-project/target",
498            1024,
499            None,
500        );
501
502        let expected = "🦀 /path/to/unnamed-project";
503        assert_eq!(format!("{rust_project}"), expected);
504
505        let node_project = create_test_project(
506            ProjectType::Node,
507            "/some/other/path",
508            "/some/other/path/node_modules",
509            2048,
510            None,
511        );
512
513        let expected = "📦 /some/other/path";
514        assert_eq!(format!("{node_project}"), expected);
515    }
516
517    #[test]
518    fn test_project_clone() {
519        let original = create_test_project(
520            ProjectType::Rust,
521            "/original/path",
522            "/original/path/target",
523            1024,
524            Some("original-project".to_string()),
525        );
526
527        let cloned = original.clone();
528
529        assert_eq!(original.kind, cloned.kind);
530        assert_eq!(original.root_path, cloned.root_path);
531        assert_eq!(original.build_arts[0].path, cloned.build_arts[0].path);
532        assert_eq!(original.build_arts[0].size, cloned.build_arts[0].size);
533        assert_eq!(original.name, cloned.name);
534    }
535
536    #[test]
537    fn test_build_artifacts_clone() {
538        let original = create_test_build_artifacts("/test/path", 2048);
539        let cloned = original.clone();
540
541        assert_eq!(original.path, cloned.path);
542        assert_eq!(original.size, cloned.size);
543    }
544
545    #[test]
546    fn test_project_with_zero_size() {
547        let project = create_test_project(
548            ProjectType::Python,
549            "/empty/project",
550            "/empty/project/__pycache__",
551            0,
552            Some("empty-project".to_string()),
553        );
554
555        assert_eq!(project.total_size(), 0);
556        assert_eq!(format!("{project}"), "🐍 empty-project (/empty/project)");
557    }
558
559    #[test]
560    fn test_project_with_large_size() {
561        let large_size = u64::MAX;
562        let project = create_test_project(
563            ProjectType::Go,
564            "/large/project",
565            "/large/project/vendor",
566            large_size,
567            Some("huge-project".to_string()),
568        );
569
570        assert_eq!(project.total_size(), large_size);
571    }
572}