Skip to main content

dix/
json.rs

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
24/// Writes the diff report as JSON.
25///
26/// # Errors
27///
28/// Returns an error if querying the diff report or writing JSON fails.
29pub 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  /// package changes
46  diffs:    Vec<JsonDiff<'a>>,
47  /// exact closure path counts
48  paths:    JsonPathStats,
49  /// old closure size (in bytes)
50  size_old: i64,
51  /// new closure size (in bytes)
52  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}