1use std::{
2 io::Write,
3 path::Path,
4};
5
6use eyre::{
7 Result,
8 WrapErr as _,
9};
10use serde::Serialize;
11
12use crate::{
13 DerivationSelectionStatus,
14 DiffReport,
15 DiffStatus,
16 PackageDiff,
17 PathStats,
18 Version,
19 VersionAmount,
20 VersionDiff,
21 query_diff_report,
22};
23
24pub fn display_diff(
30 path_old: &Path,
31 path_new: &Path,
32 force_correctness: bool,
33) -> Result<()> {
34 let report = query_diff_report(path_old, path_new, force_correctness)?;
35 generate_diff(&mut std::io::stdout(), &report)
36}
37
38fn generate_diff(out: &mut dyn Write, report: &DiffReport) -> Result<()> {
39 serde_json::to_writer(out, &JsonReport::from(report))
40 .context("Failed to write json output.")
41}
42
43#[derive(Serialize)]
44pub struct JsonReport<'a> {
45 diffs: Vec<JsonDiff<'a>>,
47 paths: JsonPathStats,
49 size_old: i64,
51 size_new: i64,
53}
54
55impl<'a> From<&'a DiffReport> for JsonReport<'a> {
56 fn from(report: &'a DiffReport) -> Self {
57 Self {
58 diffs: report.diffs().iter().map(JsonDiff::from).collect(),
59 paths: JsonPathStats::from(report.path_stats()),
60 size_old: report.size_old().bytes(),
61 size_new: report.size_new().bytes(),
62 }
63 }
64}
65
66#[derive(Serialize)]
67struct JsonPathStats {
68 old: usize,
69 new: usize,
70 added: usize,
71 removed: usize,
72}
73
74impl From<PathStats> for JsonPathStats {
75 fn from(stats: PathStats) -> Self {
76 Self {
77 old: stats.old_count(),
78 new: stats.new_count(),
79 added: stats.added_count(),
80 removed: stats.removed_count(),
81 }
82 }
83}
84
85#[derive(Serialize)]
86struct JsonDiff<'a> {
87 name: &'a str,
88 versions: Vec<JsonVersionDiff<'a>>,
89 status: JsonDiffStatus,
90 selection: JsonDerivationSelectionStatus,
91 has_omitted_versions: bool,
92 size_old: i64,
93 size_new: i64,
94 size_delta: i64,
95}
96
97impl<'a> From<&'a PackageDiff> for JsonDiff<'a> {
98 fn from(diff: &'a PackageDiff) -> Self {
99 let size_delta = diff.size.delta();
100 Self {
101 name: diff.name.as_str(),
102 versions: diff
103 .versions
104 .iter()
105 .map(JsonVersionDiff::from)
106 .collect(),
107 status: JsonDiffStatus::from(diff.status),
108 selection: JsonDerivationSelectionStatus::from(diff.selection),
109 has_omitted_versions: diff.has_omitted_versions,
110 size_old: diff.size.old_size().bytes(),
111 size_new: diff.size.new_size().bytes(),
112 size_delta: size_delta.bytes(),
113 }
114 }
115}
116
117#[derive(Serialize)]
118struct JsonVersion<'a> {
119 name: &'a str,
120}
121
122impl<'a> From<&'a Version> for JsonVersion<'a> {
123 fn from(version: &'a Version) -> Self {
124 Self {
125 name: version.name.as_str(),
126 }
127 }
128}
129
130#[derive(Serialize)]
131struct JsonVersionAmount<'a> {
132 name: &'a str,
133 amount: usize,
134}
135
136impl<'a> From<&'a VersionAmount> for JsonVersionAmount<'a> {
137 fn from(version: &'a VersionAmount) -> Self {
138 Self {
139 name: version.version.name.as_str(),
140 amount: version.amount.get(),
141 }
142 }
143}
144
145#[derive(Serialize)]
146#[serde(tag = "kind", rename_all = "snake_case")]
147enum JsonVersionDiff<'a> {
148 Removed {
149 version: JsonVersionAmount<'a>,
150 },
151 Added {
152 version: JsonVersionAmount<'a>,
153 },
154 Changed {
155 old: JsonVersionAmount<'a>,
156 new: JsonVersionAmount<'a>,
157 },
158 AmountChanged {
159 version: JsonVersion<'a>,
160 old_amount: usize,
161 new_amount: usize,
162 },
163}
164
165impl<'a> From<&'a VersionDiff> for JsonVersionDiff<'a> {
166 fn from(version_diff: &'a VersionDiff) -> Self {
167 match version_diff {
168 VersionDiff::Removed(version) => {
169 Self::Removed {
170 version: JsonVersionAmount::from(version),
171 }
172 },
173 VersionDiff::Added(version) => {
174 Self::Added {
175 version: JsonVersionAmount::from(version),
176 }
177 },
178 VersionDiff::Changed { old, new } => {
179 Self::Changed {
180 old: JsonVersionAmount::from(old),
181 new: JsonVersionAmount::from(new),
182 }
183 },
184 VersionDiff::AmountChanged {
185 version,
186 old_amount,
187 new_amount,
188 } => {
189 Self::AmountChanged {
190 version: JsonVersion::from(version),
191 old_amount: old_amount.get(),
192 new_amount: new_amount.get(),
193 }
194 },
195 }
196 }
197}
198
199#[derive(Serialize)]
200enum JsonDiffStatus {
201 Changed,
202 Mixed,
203 Upgraded,
204 Downgraded,
205 Added,
206 Removed,
207}
208
209impl From<DiffStatus> for JsonDiffStatus {
210 fn from(status: DiffStatus) -> Self {
211 match status {
212 DiffStatus::Changed => Self::Changed,
213 DiffStatus::Mixed => Self::Mixed,
214 DiffStatus::Upgraded => Self::Upgraded,
215 DiffStatus::Downgraded => Self::Downgraded,
216 DiffStatus::Added => Self::Added,
217 DiffStatus::Removed => Self::Removed,
218 }
219 }
220}
221
222#[derive(Serialize)]
223enum JsonDerivationSelectionStatus {
224 Selected,
225 NewlySelected,
226 Unselected,
227 NewlyUnselected,
228}
229
230impl From<DerivationSelectionStatus> for JsonDerivationSelectionStatus {
231 fn from(status: DerivationSelectionStatus) -> Self {
232 match status {
233 DerivationSelectionStatus::Selected => Self::Selected,
234 DerivationSelectionStatus::NewlySelected => Self::NewlySelected,
235 DerivationSelectionStatus::Unselected => Self::Unselected,
236 DerivationSelectionStatus::NewlyUnselected => Self::NewlyUnselected,
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use std::num::NonZeroUsize;
244
245 use size::Size;
246
247 use super::*;
248 use crate::{
249 DerivationSelectionStatus,
250 DiffReport,
251 PackageDiff,
252 PackageSizeDelta,
253 };
254
255 fn amount(amount: usize) -> NonZeroUsize {
256 NonZeroUsize::new(amount)
257 .unwrap_or_else(|| panic!("test version amount must be nonzero"))
258 }
259
260 #[test]
261 fn test_basic_json_output_format() {
262 let expected_output = r#"{"diffs":[{"name":"nixos","versions":[{"kind":"changed","old":{"name":"25.11-system-path","amount":1},"new":{"name":"25.12-system-path","amount":1}},{"kind":"amount_changed","version":{"name":"25.12-system"},"old_amount":1,"new_amount":2}],"status":"Changed","selection":"Unselected","has_omitted_versions":false,"size_old":1000,"size_new":2500,"size_delta":1500}],"paths":{"old":7529,"new":7536,"added":5054,"removed":5047},"size_old":115001000,"size_new":115001000}"#;
263
264 let report = DiffReport::new_for_test(
265 vec![PackageDiff {
266 name: "nixos".to_owned(),
267 versions: vec![
268 VersionDiff::Changed {
269 old: VersionAmount::new("25.11-system-path", amount(1)),
270 new: VersionAmount::new("25.12-system-path", amount(1)),
271 },
272 VersionDiff::AmountChanged {
273 version: Version::new("25.12-system"),
274 old_amount: amount(1),
275 new_amount: amount(2),
276 },
277 ],
278 status: DiffStatus::Changed,
279 selection: DerivationSelectionStatus::Unselected,
280 has_omitted_versions: false,
281 size: PackageSizeDelta::new(
282 Size::from_bytes(1_000),
283 Size::from_bytes(2_500),
284 ),
285 }],
286 PathStats::new_for_test(7529, 7536, 5054, 5047),
287 Size::from_bytes(115_001_000),
288 Size::from_bytes(115_001_000),
289 );
290
291 let mut actual_output = Vec::new();
292 generate_diff(&mut actual_output, &report).unwrap();
293 let actual_output = String::from_utf8(actual_output).unwrap();
294 assert_eq!(expected_output, &actual_output);
295 }
296}