Skip to main content

sbom_diff/renderer/
json.rs

1use super::{RenderOptions, Renderer, SummaryRenderer};
2use crate::{Diff, EcosystemCounts, GroupedDiff};
3use serde::Serialize;
4use std::collections::BTreeMap;
5use std::io::Write;
6
7/// JSON renderer for machine consumption.
8///
9/// outputs the [`Diff`] struct as pretty-printed JSON. When
10/// `group_by_ecosystem` is set, the output includes an
11/// `ecosystem_breakdown` field with per-ecosystem counts and the
12/// `by_ecosystem` field with grouped component data.
13pub struct JsonRenderer;
14
15/// wrapper for JSON output that optionally includes ecosystem breakdown.
16#[derive(Serialize)]
17struct JsonOutput<'a> {
18    #[serde(flatten)]
19    diff: &'a Diff,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    ecosystem_breakdown: Option<BTreeMap<String, EcosystemCounts>>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    by_ecosystem: Option<&'a GroupedDiff>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    warnings: Option<JsonWarnings<'a>>,
26}
27
28#[derive(Serialize)]
29struct JsonWarnings<'a> {
30    old: &'a Vec<String>,
31    new: &'a Vec<String>,
32}
33
34impl Renderer for JsonRenderer {
35    fn render<W: Write>(
36        &self,
37        diff: &Diff,
38        opts: &RenderOptions,
39        writer: &mut W,
40    ) -> anyhow::Result<()> {
41        let warnings = if opts.has_warnings() {
42            Some(JsonWarnings {
43                old: &opts.old_warnings,
44                new: &opts.new_warnings,
45            })
46        } else {
47            None
48        };
49
50        if opts.group_by_ecosystem {
51            let grouped = diff.group_by_ecosystem();
52            let output = JsonOutput {
53                diff,
54                ecosystem_breakdown: Some(grouped.ecosystem_breakdown()),
55                by_ecosystem: Some(&grouped),
56                warnings,
57            };
58            serde_json::to_writer_pretty(writer, &output)?;
59        } else {
60            let output = JsonOutput {
61                diff,
62                ecosystem_breakdown: None,
63                by_ecosystem: None,
64                warnings,
65            };
66            serde_json::to_writer_pretty(writer, &output)?;
67        }
68        Ok(())
69    }
70}
71
72impl SummaryRenderer for JsonRenderer {
73    fn render_summary<W: Write>(
74        &self,
75        diff: &Diff,
76        opts: &RenderOptions,
77        writer: &mut W,
78    ) -> anyhow::Result<()> {
79        let mut summary = serde_json::json!({
80            "old_total": diff.old_total,
81            "new_total": diff.new_total,
82            "unchanged": diff.unchanged,
83            "added": diff.added.len(),
84            "removed": diff.removed.len(),
85            "changed": diff.changed.len(),
86            "edge_changes": diff.edge_diffs.len(),
87            "metadata_changed": diff.metadata_changed.is_some(),
88        });
89
90        if let Some(mc) = &diff.metadata_changed {
91            summary["metadata_changes"] = serde_json::to_value(mc)?;
92        }
93
94        if opts.has_warnings() {
95            summary["warnings"] = serde_json::json!({
96                "old": opts.old_warnings,
97                "new": opts.new_warnings,
98            });
99        }
100
101        if opts.group_by_ecosystem {
102            let breakdown = diff.ecosystem_breakdown();
103            if !breakdown.is_empty() {
104                summary["ecosystem_breakdown"] = serde_json::to_value(&breakdown)?;
105            }
106        }
107
108        serde_json::to_writer_pretty(writer, &summary)
109            .map_err(|e| anyhow::anyhow!("json summary: {}", e))
110    }
111}