1use super::{
2 format_option, format_set, format_vec_or_none, RenderOptions, Renderer, SummaryRenderer,
3};
4use crate::{Diff, FieldChange};
5use std::io::Write;
6
7fn csv_writer<W: Write>(writer: W) -> csv::Writer<W> {
10 csv::WriterBuilder::new()
11 .terminator(csv::Terminator::Any(b'\n'))
12 .from_writer(writer)
13}
14
15pub struct CsvRenderer;
22
23impl Renderer for CsvRenderer {
24 fn render<W: Write>(
25 &self,
26 diff: &Diff,
27 opts: &RenderOptions,
28 writer: &mut W,
29 ) -> anyhow::Result<()> {
30 let mut wtr = csv_writer(&mut *writer);
31
32 wtr.write_record([
33 "status",
34 "component",
35 "ecosystem",
36 "field",
37 "old_value",
38 "new_value",
39 ])?;
40
41 if opts.has_warnings() {
42 for w in &opts.old_warnings {
43 wtr.write_record(["warning", "old", "", "", w, ""])?;
44 }
45 for w in &opts.new_warnings {
46 wtr.write_record(["warning", "new", "", "", w, ""])?;
47 }
48 }
49
50 for comp in &diff.added {
51 let display = comp.purl.as_deref().unwrap_or(comp.id.as_str());
52 let eco = comp.ecosystem.as_deref().unwrap_or("");
53 let ver = comp.version.as_deref().unwrap_or("");
54 wtr.write_record(["added", display, eco, "version", "", ver])?;
55 }
56
57 for comp in &diff.removed {
58 let display = comp.purl.as_deref().unwrap_or(comp.id.as_str());
59 let eco = comp.ecosystem.as_deref().unwrap_or("");
60 let ver = comp.version.as_deref().unwrap_or("");
61 wtr.write_record(["removed", display, eco, "version", ver, ""])?;
62 }
63
64 for change in &diff.changed {
65 let display = change.new.purl.as_deref().unwrap_or(change.id.as_str());
66 let eco = change.new.ecosystem.as_deref().unwrap_or("");
67 for fc in &change.changes {
68 let (field, old, new) = csv_field_change(fc);
69 wtr.write_record(["changed", display, eco, field, &old, &new])?;
70 }
71 }
72
73 for edge in &diff.edge_diffs {
74 let parent = diff.display_name(&edge.parent);
75 for (child, kind) in &edge.added {
76 let child_name = diff.display_name(child);
77 wtr.write_record(["edge-added", parent, "", child_name, "", &kind.to_string()])?;
78 }
79 for (child, kind) in &edge.removed {
80 let child_name = diff.display_name(child);
81 wtr.write_record([
82 "edge-removed",
83 parent,
84 "",
85 child_name,
86 &kind.to_string(),
87 "",
88 ])?;
89 }
90 for (child, (old_kind, new_kind)) in &edge.kind_changed {
91 let child_name = diff.display_name(child);
92 wtr.write_record([
93 "edge-kind-changed",
94 parent,
95 "",
96 child_name,
97 &old_kind.to_string(),
98 &new_kind.to_string(),
99 ])?;
100 }
101 }
102
103 if let Some(mc) = &diff.metadata_changed {
104 if let Some((ref old, ref new)) = mc.timestamp {
105 wtr.write_record([
106 "metadata",
107 "",
108 "",
109 "timestamp",
110 old.as_deref().unwrap_or(""),
111 new.as_deref().unwrap_or(""),
112 ])?;
113 }
114 if let Some((ref old, ref new)) = mc.tools {
115 wtr.write_record([
116 "metadata",
117 "",
118 "",
119 "tools",
120 &format_vec_or_none(old),
121 &format_vec_or_none(new),
122 ])?;
123 }
124 if let Some((ref old, ref new)) = mc.authors {
125 wtr.write_record([
126 "metadata",
127 "",
128 "",
129 "authors",
130 &format_vec_or_none(old),
131 &format_vec_or_none(new),
132 ])?;
133 }
134 }
135
136 wtr.flush()?;
137 Ok(())
138 }
139}
140
141impl SummaryRenderer for CsvRenderer {
142 fn render_summary<W: Write>(
143 &self,
144 diff: &Diff,
145 opts: &RenderOptions,
146 writer: &mut W,
147 ) -> anyhow::Result<()> {
148 let meta_changed = if diff.metadata_changed.is_some() {
149 "1"
150 } else {
151 "0"
152 };
153
154 let mut wtr = csv_writer(&mut *writer);
155 wtr.write_record(["metric", "count"])?;
156 wtr.write_record(["old_total", &diff.old_total.to_string()])?;
157 wtr.write_record(["new_total", &diff.new_total.to_string()])?;
158 wtr.write_record(["unchanged", &diff.unchanged.to_string()])?;
159 wtr.write_record(["added", &diff.added.len().to_string()])?;
160 wtr.write_record(["removed", &diff.removed.len().to_string()])?;
161 wtr.write_record(["changed", &diff.changed.len().to_string()])?;
162 wtr.write_record(["edge_changes", &diff.edge_diffs.len().to_string()])?;
163 wtr.write_record(["metadata_changed", meta_changed])?;
164
165 wtr.flush()?;
166 drop(wtr);
167
168 if opts.group_by_ecosystem {
169 let breakdown = diff.ecosystem_breakdown();
170 if !breakdown.is_empty() {
171 writeln!(writer)?;
172 let mut wtr = csv_writer(&mut *writer);
173 wtr.write_record(["ecosystem", "added", "removed", "changed"])?;
174 for (eco, counts) in &breakdown {
175 wtr.write_record([
176 eco.as_str(),
177 &counts.added.to_string(),
178 &counts.removed.to_string(),
179 &counts.changed.to_string(),
180 ])?;
181 }
182 wtr.flush()?;
183 }
184 }
185
186 Ok(())
187 }
188}
189
190fn csv_field_change(fc: &FieldChange) -> (&'static str, String, String) {
192 match fc {
193 FieldChange::Version(old, new) => (
194 "version",
195 format_option(old).to_string(),
196 format_option(new).to_string(),
197 ),
198 FieldChange::License(old, new) => ("license", format_set(old), format_set(new)),
199 FieldChange::Supplier(old, new) => (
200 "supplier",
201 format_option(old).to_string(),
202 format_option(new).to_string(),
203 ),
204 FieldChange::Purl(old, new) => (
205 "purl",
206 format_option(old).to_string(),
207 format_option(new).to_string(),
208 ),
209 FieldChange::Description(old, new) => (
210 "description",
211 format_option(old).to_string(),
212 format_option(new).to_string(),
213 ),
214 FieldChange::Hashes(old, new) => {
215 let old_str = old
216 .iter()
217 .map(|(k, v)| format!("{}={}", k, v))
218 .collect::<Vec<_>>()
219 .join("; ");
220 let new_str = new
221 .iter()
222 .map(|(k, v)| format!("{}={}", k, v))
223 .collect::<Vec<_>>()
224 .join("; ");
225 ("hashes", old_str, new_str)
226 }
227 FieldChange::Ecosystem(old, new) => (
228 "ecosystem",
229 format_option(old).to_string(),
230 format_option(new).to_string(),
231 ),
232 }
233}