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}