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}