1pub mod analyst;
19pub mod csaf;
20mod csv;
21pub mod escape;
22mod html;
23mod json;
24mod markdown;
25pub mod oscal;
26mod sarif;
27pub mod sbomqs_compat;
28mod sidebyside;
29pub mod streaming;
30mod summary;
31mod types;
32
33pub use csaf::{CsafEmitOptions, emit_csaf};
34pub use csv::CsvReporter;
35pub use html::HtmlReporter;
36pub use json::JsonReporter;
37pub use markdown::MarkdownReporter;
38pub use sarif::SarifReporter;
39pub use sarif::{
40 generate_ai_readiness_sarif, generate_compliance_sarif, generate_multi_compliance_sarif,
41 generate_quality_sarif,
42};
43pub use sidebyside::SideBySideReporter;
44pub use streaming::{
45 NdjsonReportGenerator, NdjsonReporter, NdjsonWriter, StreamingJsonReporter, StreamingJsonWriter,
46};
47pub use summary::{SummaryReporter, TableReporter};
48pub use types::{MinSeverity, ReportConfig, ReportFormat, ReportMetadata, ReportType};
49
50use crate::diff::DiffResult;
54use crate::model::NormalizedSbom;
55use std::io::Write;
56use thiserror::Error;
57
58#[derive(Error, Debug)]
60pub enum ReportError {
61 #[error("IO error: {0}")]
62 IoError(#[from] std::io::Error),
63
64 #[error("Serialization error: {0}")]
65 SerializationError(String),
66
67 #[error("Template error: {0}")]
68 TemplateError(String),
69
70 #[error("Invalid configuration: {0}")]
71 ConfigError(String),
72
73 #[error("Format error: {0}")]
74 FormatError(#[from] std::fmt::Error),
75}
76
77pub trait ReportGenerator {
79 fn generate_diff_report(
81 &self,
82 result: &DiffResult,
83 old_sbom: &NormalizedSbom,
84 new_sbom: &NormalizedSbom,
85 config: &ReportConfig,
86 ) -> Result<String, ReportError>;
87
88 fn generate_view_report(
90 &self,
91 sbom: &NormalizedSbom,
92 config: &ReportConfig,
93 ) -> Result<String, ReportError>;
94
95 fn write_diff_report(
97 &self,
98 result: &DiffResult,
99 old_sbom: &NormalizedSbom,
100 new_sbom: &NormalizedSbom,
101 config: &ReportConfig,
102 writer: &mut dyn Write,
103 ) -> Result<(), ReportError> {
104 let report = self.generate_diff_report(result, old_sbom, new_sbom, config)?;
105 writer.write_all(report.as_bytes())?;
106 Ok(())
107 }
108
109 fn format(&self) -> ReportFormat;
111}
112
113pub trait WriterReporter {
135 fn write_diff_to<W: Write>(
140 &self,
141 result: &DiffResult,
142 old_sbom: &NormalizedSbom,
143 new_sbom: &NormalizedSbom,
144 config: &ReportConfig,
145 writer: &mut W,
146 ) -> Result<(), ReportError>;
147
148 fn write_view_to<W: Write>(
150 &self,
151 sbom: &NormalizedSbom,
152 config: &ReportConfig,
153 writer: &mut W,
154 ) -> Result<(), ReportError>;
155
156 fn format(&self) -> ReportFormat;
158}
159
160#[deprecated(since = "0.2.0", note = "Renamed to WriterReporter for clarity")]
162pub trait StreamingReporter: WriterReporter {}
163
164impl<T: ReportGenerator> WriterReporter for T {
171 fn write_diff_to<W: Write>(
172 &self,
173 result: &DiffResult,
174 old_sbom: &NormalizedSbom,
175 new_sbom: &NormalizedSbom,
176 config: &ReportConfig,
177 writer: &mut W,
178 ) -> Result<(), ReportError> {
179 let report = self.generate_diff_report(result, old_sbom, new_sbom, config)?;
180 writer.write_all(report.as_bytes())?;
181 Ok(())
182 }
183
184 fn write_view_to<W: Write>(
185 &self,
186 sbom: &NormalizedSbom,
187 config: &ReportConfig,
188 writer: &mut W,
189 ) -> Result<(), ReportError> {
190 let report = self.generate_view_report(sbom, config)?;
191 writer.write_all(report.as_bytes())?;
192 Ok(())
193 }
194
195 fn format(&self) -> ReportFormat {
196 ReportGenerator::format(self)
197 }
198}
199
200#[allow(deprecated)]
201impl<T: WriterReporter> StreamingReporter for T {}
202
203#[must_use]
205pub fn create_reporter(format: ReportFormat) -> Box<dyn ReportGenerator> {
206 create_reporter_with_options(format, true)
207}
208
209#[must_use]
211pub fn create_reporter_with_options(
212 format: ReportFormat,
213 use_color: bool,
214) -> Box<dyn ReportGenerator> {
215 match format {
216 ReportFormat::Auto | ReportFormat::Summary => {
217 if use_color {
218 Box::new(SummaryReporter::new())
219 } else {
220 Box::new(SummaryReporter::new().no_color())
221 }
222 }
223 ReportFormat::Json | ReportFormat::Tui => Box::new(JsonReporter::new()), ReportFormat::Sarif => Box::new(SarifReporter::new()),
225 ReportFormat::OscalJson => Box::new(JsonReporter::new()),
226 ReportFormat::Markdown => Box::new(MarkdownReporter::new()),
227 ReportFormat::Html => Box::new(HtmlReporter::new()),
228 ReportFormat::SideBySide => {
229 if use_color {
230 Box::new(SideBySideReporter::new())
231 } else {
232 Box::new(SideBySideReporter::new().no_colors())
233 }
234 }
235 ReportFormat::Table => {
236 if use_color {
237 Box::new(TableReporter::new())
238 } else {
239 Box::new(TableReporter::new().no_color())
240 }
241 }
242 ReportFormat::Csv => Box::new(CsvReporter::new()),
243 ReportFormat::Ndjson => Box::new(NdjsonReportGenerator::new()),
244 ReportFormat::SbomqsJson => Box::new(JsonReporter::new()),
249 }
250}
251
252#[cfg(test)]
253mod factory_tests {
254 use super::{ReportConfig, ReportFormat, create_reporter_with_options};
255
256 #[test]
263 fn colorable_formats_honour_use_color_false() {
264 let (diff, old, new) = crate::tui::test_support::demo_diff();
265 let config = ReportConfig::default();
266
267 for format in [
268 ReportFormat::SideBySide,
269 ReportFormat::Summary,
270 ReportFormat::Table,
271 ] {
272 let report = create_reporter_with_options(format, false)
273 .generate_diff_report(&diff, &old, &new, &config)
274 .expect("report generation must succeed");
275 assert!(
276 !report.contains('\u{1b}'),
277 "{format:?} emitted ANSI escapes with use_color=false"
278 );
279 }
280 }
281
282 #[test]
284 fn side_by_side_still_colors_when_enabled() {
285 let (diff, old, new) = crate::tui::test_support::demo_diff();
286 let report = create_reporter_with_options(ReportFormat::SideBySide, true)
287 .generate_diff_report(&diff, &old, &new, &ReportConfig::default())
288 .expect("report generation must succeed");
289 assert!(
290 report.contains('\u{1b}'),
291 "side-by-side must still color when use_color=true"
292 );
293 }
294}