Skip to main content

clean_dev_dirs/
output.rs

1//! Structured JSON output for scripting and piping.
2//!
3//! This module provides serializable data structures that represent the
4//! complete output of a scan or cleanup operation. When the `--json` flag
5//! is passed, these structures are serialized to stdout as a single JSON
6//! object, replacing all human-readable output.
7
8use std::collections::BTreeMap;
9
10use humansize::{DECIMAL, format_size};
11use serde::Serialize;
12
13use crate::project::{Project, ProjectType};
14
15/// Top-level JSON output emitted when `--json` is active.
16#[derive(Debug, Serialize)]
17pub struct JsonOutput {
18    /// The execution mode: `"dry_run"` or `"cleanup"`.
19    pub mode: String,
20
21    /// List of projects that were found (and matched filters).
22    pub projects: Vec<JsonProjectEntry>,
23
24    /// Aggregated summary statistics.
25    pub summary: JsonSummary,
26
27    /// Cleanup results. Present only when an actual cleanup was performed
28    /// (i.e. not in dry-run mode).
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub cleanup: Option<JsonCleanupResult>,
31}
32
33/// A single project entry in the JSON output.
34#[derive(Debug, Serialize)]
35pub struct JsonProjectEntry {
36    /// Project name extracted from config files, or `null`.
37    pub name: Option<String>,
38
39    /// Project type (`"rust"`, `"node"`, `"python"`, `"go"`, `"java"`, `"cpp"`, `"swift"`, `"dot_net"`).
40    #[serde(rename = "type")]
41    pub project_type: ProjectType,
42
43    /// Absolute path to the project root directory.
44    pub root_path: String,
45
46    /// Absolute paths to the build artifacts directories.
47    pub build_artifacts_paths: Vec<String>,
48
49    /// Total size of the build artifacts in bytes.
50    pub build_artifacts_size: u64,
51
52    /// Human-readable formatted size (e.g. `"1.23 GB"`).
53    pub build_artifacts_size_formatted: String,
54}
55
56/// Aggregated summary across all matched projects.
57#[derive(Debug, Serialize)]
58pub struct JsonSummary {
59    /// Total number of projects found.
60    pub total_projects: usize,
61
62    /// Total reclaimable size in bytes.
63    pub total_size: u64,
64
65    /// Human-readable formatted total size.
66    pub total_size_formatted: String,
67
68    /// Per-type breakdown (key is the project type name).
69    pub by_type: BTreeMap<String, JsonTypeSummary>,
70}
71
72/// Per-project-type count and size.
73#[derive(Debug, Serialize)]
74pub struct JsonTypeSummary {
75    /// Number of projects of this type.
76    pub count: usize,
77
78    /// Total size in bytes for this type.
79    pub size: u64,
80
81    /// Human-readable formatted size.
82    pub size_formatted: String,
83}
84
85/// Results of a cleanup operation.
86#[derive(Debug, Serialize)]
87pub struct JsonCleanupResult {
88    /// Number of projects successfully cleaned.
89    pub success_count: usize,
90
91    /// Number of projects that failed to clean.
92    pub failure_count: usize,
93
94    /// Total bytes actually freed.
95    pub total_freed: u64,
96
97    /// Human-readable formatted freed size.
98    pub total_freed_formatted: String,
99
100    /// Error messages for projects that failed.
101    pub errors: Vec<String>,
102}
103
104impl JsonOutput {
105    /// Build a `JsonOutput` from a slice of projects in dry-run mode.
106    #[must_use]
107    pub fn from_projects_dry_run(projects: &[Project]) -> Self {
108        Self {
109            mode: "dry_run".to_string(),
110            projects: projects
111                .iter()
112                .map(JsonProjectEntry::from_project)
113                .collect(),
114            summary: JsonSummary::from_projects(projects),
115            cleanup: None,
116        }
117    }
118
119    /// Build a `JsonOutput` from a slice of projects after a cleanup operation.
120    #[must_use]
121    pub fn from_projects_cleanup(
122        projects: &[Project],
123        clean_result: &crate::cleaner::CleanResult,
124    ) -> Self {
125        Self {
126            mode: "cleanup".to_string(),
127            projects: projects
128                .iter()
129                .map(JsonProjectEntry::from_project)
130                .collect(),
131            summary: JsonSummary::from_projects(projects),
132            cleanup: Some(JsonCleanupResult::from_clean_result(clean_result)),
133        }
134    }
135}
136
137impl JsonProjectEntry {
138    /// Convert a `Project` into a `JsonProjectEntry`.
139    #[must_use]
140    pub fn from_project(project: &Project) -> Self {
141        let total = project.total_size();
142        Self {
143            name: project.name.clone(),
144            project_type: project.kind.clone(),
145            root_path: project.root_path.display().to_string(),
146            build_artifacts_paths: project
147                .build_arts
148                .iter()
149                .map(|a| a.path.display().to_string())
150                .collect(),
151            build_artifacts_size: total,
152            build_artifacts_size_formatted: format_size(total, DECIMAL),
153        }
154    }
155}
156
157impl JsonSummary {
158    /// Compute summary statistics from a slice of projects.
159    #[must_use]
160    pub fn from_projects(projects: &[Project]) -> Self {
161        let mut by_type: BTreeMap<String, (usize, u64)> = BTreeMap::new();
162
163        for project in projects {
164            let key = match project.kind {
165                ProjectType::Rust => "rust",
166                ProjectType::Node => "node",
167                ProjectType::Python => "python",
168                ProjectType::Go => "go",
169                ProjectType::Java => "java",
170                ProjectType::Cpp => "cpp",
171                ProjectType::Swift => "swift",
172                ProjectType::DotNet => "dotnet",
173                ProjectType::Ruby => "ruby",
174                ProjectType::Elixir => "elixir",
175                ProjectType::Deno => "deno",
176                ProjectType::Php => "php",
177                ProjectType::Haskell => "haskell",
178                ProjectType::Dart => "dart",
179                ProjectType::Zig => "zig",
180                ProjectType::Scala => "scala",
181            };
182
183            let entry = by_type.entry(key.to_string()).or_insert((0, 0));
184            entry.0 += 1;
185            entry.1 += project.total_size();
186        }
187
188        let total_size: u64 = projects.iter().map(Project::total_size).sum();
189
190        Self {
191            total_projects: projects.len(),
192            total_size,
193            total_size_formatted: format_size(total_size, DECIMAL),
194            by_type: by_type
195                .into_iter()
196                .map(|(k, (count, size))| {
197                    (
198                        k,
199                        JsonTypeSummary {
200                            count,
201                            size,
202                            size_formatted: format_size(size, DECIMAL),
203                        },
204                    )
205                })
206                .collect(),
207        }
208    }
209}
210
211impl JsonCleanupResult {
212    /// Convert a `CleanResult` into a `JsonCleanupResult`.
213    #[must_use]
214    pub fn from_clean_result(result: &crate::cleaner::CleanResult) -> Self {
215        Self {
216            success_count: result.success_count,
217            failure_count: result.errors.len(),
218            total_freed: result.total_freed,
219            total_freed_formatted: format_size(result.total_freed, DECIMAL),
220            errors: result.errors.clone(),
221        }
222    }
223}