1mod csv_format;
12mod json;
13mod markdown;
14mod sarif;
15mod text;
16
17pub use csv_format::CsvRenderer;
18pub use json::JsonRenderer;
19pub use markdown::MarkdownRenderer;
20pub use sarif::SarifRenderer;
21pub use text::TextRenderer;
22
23use crate::{ComponentChange, Diff, EcosystemCounts, FieldChange};
24use sbom_model::DependencyKind;
25use std::collections::{BTreeMap, BTreeSet};
26use std::io::Write;
27
28#[derive(Debug, Clone, Default)]
30pub struct RenderOptions {
31 pub group_by_ecosystem: bool,
33 pub show_warnings: bool,
35 pub old_warnings: Vec<String>,
37 pub new_warnings: Vec<String>,
39}
40
41impl RenderOptions {
42 pub fn has_warnings(&self) -> bool {
44 self.show_warnings && (!self.old_warnings.is_empty() || !self.new_warnings.is_empty())
45 }
46
47 pub fn warning_count(&self) -> usize {
49 self.old_warnings.len() + self.new_warnings.len()
50 }
51}
52
53pub(super) fn kind_suffix(kind: &DependencyKind) -> &'static str {
56 match kind {
57 DependencyKind::Runtime => "",
58 DependencyKind::Dev => " (dev)",
59 DependencyKind::Build => " (build)",
60 DependencyKind::Test => " (test)",
61 DependencyKind::Optional => " (optional)",
62 DependencyKind::Provided => " (provided)",
63 }
64}
65
66pub fn format_option(opt: &Option<String>) -> &str {
68 opt.as_deref().unwrap_or("<none>")
69}
70
71pub fn format_set(set: &BTreeSet<String>) -> String {
73 if set.is_empty() {
74 "<none>".to_string()
75 } else {
76 let mut out = String::new();
77 for (i, s) in set.iter().enumerate() {
78 if i > 0 {
79 out.push_str(", ");
80 }
81 out.push_str(s);
82 }
83 out
84 }
85}
86
87pub trait Renderer {
89 fn render<W: Write>(
91 &self,
92 diff: &Diff,
93 opts: &RenderOptions,
94 writer: &mut W,
95 ) -> anyhow::Result<()>;
96}
97
98pub trait SummaryRenderer {
102 fn render_summary<W: Write>(
104 &self,
105 diff: &Diff,
106 opts: &RenderOptions,
107 writer: &mut W,
108 ) -> anyhow::Result<()>;
109}
110
111pub(super) trait FieldChangeFormatter {
114 fn field_change<W: Write>(
115 &self,
116 w: &mut W,
117 name: &str,
118 old: &str,
119 new: &str,
120 ) -> std::io::Result<()>;
121 fn hash_header<W: Write>(&self, w: &mut W) -> std::io::Result<()>;
122 fn hash_removed<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()>;
123 fn hash_changed<W: Write>(
124 &self,
125 w: &mut W,
126 algo: &str,
127 old: &str,
128 new: &str,
129 ) -> std::io::Result<()>;
130 fn hash_added<W: Write>(&self, w: &mut W, algo: &str, digest: &str) -> std::io::Result<()>;
131 fn component_header<W: Write>(&self, w: &mut W, id: &str) -> std::io::Result<()>;
132}
133
134pub(super) fn write_field_changes<F: FieldChangeFormatter, W: Write>(
135 fmt: &F,
136 writer: &mut W,
137 changes: &[FieldChange],
138) -> std::io::Result<()> {
139 for change in changes {
140 match change {
141 FieldChange::Version(old, new) => {
142 fmt.field_change(writer, "Version", format_option(old), format_option(new))?;
143 }
144 FieldChange::License(old, new) => {
145 fmt.field_change(writer, "License", &format_set(old), &format_set(new))?;
146 }
147 FieldChange::Supplier(old, new) => {
148 fmt.field_change(writer, "Supplier", format_option(old), format_option(new))?;
149 }
150 FieldChange::Purl(old, new) => {
151 fmt.field_change(writer, "Purl", format_option(old), format_option(new))?;
152 }
153 FieldChange::Description(old, new) => {
154 fmt.field_change(
155 writer,
156 "Description",
157 format_option(old),
158 format_option(new),
159 )?;
160 }
161 FieldChange::Hashes(old, new) => {
162 fmt.hash_header(writer)?;
163 for (algo, digest) in old {
164 if !new.contains_key(algo) {
165 fmt.hash_removed(writer, algo, digest)?;
166 } else if new[algo] != *digest {
167 fmt.hash_changed(writer, algo, digest, &new[algo])?;
168 }
169 }
170 for (algo, digest) in new {
171 if !old.contains_key(algo) {
172 fmt.hash_added(writer, algo, digest)?;
173 }
174 }
175 }
176 FieldChange::Ecosystem(old, new) => {
177 fmt.field_change(writer, "Ecosystem", format_option(old), format_option(new))?;
178 }
179 }
180 }
181 Ok(())
182}
183
184pub(super) fn write_changed<F: FieldChangeFormatter, W: Write>(
185 fmt: &F,
186 writer: &mut W,
187 changes: &[ComponentChange],
188) -> std::io::Result<()> {
189 for c in changes {
190 fmt.component_header(writer, c.new.purl.as_deref().unwrap_or(c.id.as_str()))?;
191 write_field_changes(fmt, writer, &c.changes)?;
192 }
193 Ok(())
194}
195
196pub(super) trait SummaryFormatter {
203 fn write_warnings<W: Write>(&self, w: &mut W, opts: &RenderOptions) -> std::io::Result<()>;
204 fn write_counts<W: Write>(&self, w: &mut W, diff: &Diff) -> std::io::Result<()>;
205 fn write_ecosystem_breakdown<W: Write>(
206 &self,
207 w: &mut W,
208 breakdown: &BTreeMap<String, EcosystemCounts>,
209 ) -> std::io::Result<()>;
210}
211
212pub(super) fn write_summary<F: SummaryFormatter, W: Write>(
213 fmt: &F,
214 diff: &Diff,
215 opts: &RenderOptions,
216 writer: &mut W,
217) -> std::io::Result<()> {
218 if opts.has_warnings() {
219 fmt.write_warnings(writer, opts)?;
220 }
221 fmt.write_counts(writer, diff)?;
222 if opts.group_by_ecosystem {
223 let breakdown = diff.ecosystem_breakdown();
224 if !breakdown.is_empty() {
225 fmt.write_ecosystem_breakdown(writer, &breakdown)?;
226 }
227 }
228 Ok(())
229}
230
231pub(super) fn format_vec_or_none(v: &[String]) -> String {
232 if v.is_empty() {
233 "<none>".to_string()
234 } else {
235 v.join(", ")
236 }
237}
238
239#[cfg(test)]
240mod tests;