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}