1use crate::{Diff, FieldChange};
10use std::io::Write;
11
12pub trait Renderer {
14 fn render<W: Write>(&self, diff: &Diff, writer: &mut W) -> anyhow::Result<()>;
16}
17
18pub struct TextRenderer;
20
21impl Renderer for TextRenderer {
22 fn render<W: Write>(&self, diff: &Diff, writer: &mut W) -> anyhow::Result<()> {
23 writeln!(writer, "Diff Summary")?;
24 writeln!(writer, "============")?;
25 writeln!(writer, "Added: {}", diff.added.len())?;
26 writeln!(writer, "Removed: {}", diff.removed.len())?;
27 writeln!(writer, "Changed: {}", diff.changed.len())?;
28 writeln!(writer)?;
29
30 if !diff.added.is_empty() {
31 writeln!(writer, "[+] Added")?;
32 writeln!(writer, "---------")?;
33 for c in &diff.added {
34 writeln!(writer, "{}", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
35 }
36 writeln!(writer)?;
37 }
38
39 if !diff.removed.is_empty() {
40 writeln!(writer, "[-] Removed")?;
41 writeln!(writer, "-----------")?;
42 for c in &diff.removed {
43 writeln!(writer, "{}", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
44 }
45 writeln!(writer)?;
46 }
47
48 if !diff.changed.is_empty() {
49 writeln!(writer, "[~] Changed")?;
50 writeln!(writer, "-----------")?;
51 for c in &diff.changed {
52 writeln!(writer, "{}", c.new.purl.as_deref().unwrap_or(c.id.as_str()))?;
53 for change in &c.changes {
54 match change {
55 FieldChange::Version(old, new) => {
56 writeln!(writer, " Version: {} -> {}", old, new)?;
57 }
58 FieldChange::License(old, new) => {
59 writeln!(writer, " License: {:?} -> {:?}", old, new)?;
60 }
61 FieldChange::Supplier(old, new) => {
62 writeln!(writer, " Supplier: {:?} -> {:?}", old, new)?;
63 }
64 FieldChange::Purl(old, new) => {
65 writeln!(writer, " Purl: {:?} -> {:?}", old, new)?;
66 }
67 FieldChange::Hashes => {
68 writeln!(writer, " Hashes: changed")?;
69 }
70 }
71 }
72 }
73 writeln!(writer)?;
74 }
75
76 if !diff.edge_diffs.is_empty() {
77 writeln!(writer, "[~] Edge Changes")?;
78 writeln!(writer, "----------------")?;
79 for edge in &diff.edge_diffs {
80 writeln!(writer, "{}", edge.parent)?;
81 for removed in &edge.removed {
82 writeln!(writer, " - {}", removed)?;
83 }
84 for added in &edge.added {
85 writeln!(writer, " + {}", added)?;
86 }
87 }
88 }
89
90 Ok(())
91 }
92}
93
94pub struct MarkdownRenderer;
98
99impl Renderer for MarkdownRenderer {
100 fn render<W: Write>(&self, diff: &Diff, writer: &mut W) -> anyhow::Result<()> {
101 writeln!(writer, "### SBOM Diff Summary")?;
102 writeln!(writer)?;
103 writeln!(writer, "| Change | Count |")?;
104 writeln!(writer, "| --- | --- |")?;
105 writeln!(writer, "| Added | {} |", diff.added.len())?;
106 writeln!(writer, "| Removed | {} |", diff.removed.len())?;
107 writeln!(writer, "| Changed | {} |", diff.changed.len())?;
108 writeln!(writer)?;
109
110 if !diff.added.is_empty() {
111 writeln!(
112 writer,
113 "<details><summary><b>Added ({})</b></summary>",
114 diff.added.len()
115 )?;
116 writeln!(writer)?;
117 for c in &diff.added {
118 writeln!(writer, "- `{}`", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
119 }
120 writeln!(writer, "</details>")?;
121 writeln!(writer)?;
122 }
123
124 if !diff.removed.is_empty() {
125 writeln!(
126 writer,
127 "<details><summary><b>Removed ({})</b></summary>",
128 diff.removed.len()
129 )?;
130 writeln!(writer)?;
131 for c in &diff.removed {
132 writeln!(writer, "- `{}`", c.purl.as_deref().unwrap_or(c.id.as_str()))?;
133 }
134 writeln!(writer, "</details>")?;
135 writeln!(writer)?;
136 }
137
138 if !diff.changed.is_empty() {
139 writeln!(
140 writer,
141 "<details><summary><b>Changed ({})</b></summary>",
142 diff.changed.len()
143 )?;
144 writeln!(writer)?;
145 for c in &diff.changed {
146 writeln!(
147 writer,
148 "#### `{}`",
149 c.new.purl.as_deref().unwrap_or(c.id.as_str())
150 )?;
151 for change in &c.changes {
152 match change {
153 FieldChange::Version(old, new) => {
154 writeln!(writer, "- **Version**: `{}` → `{}`", old, new)?;
155 }
156 FieldChange::License(old, new) => {
157 writeln!(writer, "- **License**: `{:?}` → `{:?}`", old, new)?;
158 }
159 FieldChange::Supplier(old, new) => {
160 writeln!(writer, "- **Supplier**: `{:?}` → `{:?}`", old, new)?;
161 }
162 FieldChange::Purl(old, new) => {
163 writeln!(writer, "- **Purl**: `{:?}` → `{:?}`", old, new)?;
164 }
165 FieldChange::Hashes => {
166 writeln!(writer, "- **Hashes**: changed")?;
167 }
168 }
169 }
170 }
171 writeln!(writer, "</details>")?;
172 writeln!(writer)?;
173 }
174
175 if !diff.edge_diffs.is_empty() {
176 writeln!(
177 writer,
178 "<details><summary><b>Edge Changes ({})</b></summary>",
179 diff.edge_diffs.len()
180 )?;
181 writeln!(writer)?;
182 for edge in &diff.edge_diffs {
183 writeln!(writer, "#### `{}`", edge.parent)?;
184 if !edge.removed.is_empty() {
185 writeln!(writer, "**Removed dependencies:**")?;
186 for removed in &edge.removed {
187 writeln!(writer, "- `{}`", removed)?;
188 }
189 }
190 if !edge.added.is_empty() {
191 writeln!(writer, "**Added dependencies:**")?;
192 for added in &edge.added {
193 writeln!(writer, "- `{}`", added)?;
194 }
195 }
196 writeln!(writer)?;
197 }
198 writeln!(writer, "</details>")?;
199 }
200
201 Ok(())
202 }
203}
204
205pub struct JsonRenderer;
209
210impl Renderer for JsonRenderer {
211 fn render<W: Write>(&self, diff: &Diff, writer: &mut W) -> anyhow::Result<()> {
212 serde_json::to_writer_pretty(writer, diff)?;
213 Ok(())
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use crate::{ComponentChange, Diff, FieldChange};
221 use sbom_model::Component;
222
223 fn mock_diff() -> Diff {
224 let c1 = Component::new("pkg-a".into(), Some("1.0".into()));
225 let mut c2 = c1.clone();
226 c2.version = Some("1.1".into());
227
228 Diff {
229 added: vec![Component::new("pkg-b".into(), Some("2.0".into()))],
230 removed: vec![Component::new("pkg-c".into(), Some("3.0".into()))],
231 changed: vec![ComponentChange {
232 id: c2.id.clone(),
233 old: c1,
234 new: c2,
235 changes: vec![FieldChange::Version("1.0".into(), "1.1".into())],
236 }],
237 edge_diffs: vec![],
238 metadata_changed: false,
239 }
240 }
241
242 #[test]
243 fn test_text_renderer() {
244 let diff = mock_diff();
245 let mut buf = Vec::new();
246 TextRenderer.render(&diff, &mut buf).unwrap();
247 let out = String::from_utf8(buf).unwrap();
248 assert!(out.contains("Diff Summary"));
249 assert!(out.contains("[+] Added"));
250 assert!(out.contains("[-] Removed"));
251 assert!(out.contains("[~] Changed"));
252 }
253
254 #[test]
255 fn test_markdown_renderer() {
256 let diff = mock_diff();
257 let mut buf = Vec::new();
258 MarkdownRenderer.render(&diff, &mut buf).unwrap();
259 let out = String::from_utf8(buf).unwrap();
260 assert!(out.contains("### SBOM Diff Summary"));
261 assert!(out.contains("<details>"));
262 }
263
264 #[test]
265 fn test_json_renderer() {
266 let diff = mock_diff();
267 let mut buf = Vec::new();
268 JsonRenderer.render(&diff, &mut buf).unwrap();
269 let _: serde_json::Value = serde_json::from_slice(&buf).unwrap();
270 }
271}