use super::{ReportableCrate, common};
use crate::Result;
use crate::expr::{Appraisal, Risk};
use crate::metrics::{MetricCategory, MetricValue};
use rust_xlsxwriter::{Color, DocProperties, Format, FormatAlign, Workbook};
use std::io::Write;
use strum::IntoEnumIterator;
#[expect(unused_results, reason = "rust_xlsxwriter methods return &mut Worksheet for chaining")]
pub fn generate<W: Write>(crates: &[ReportableCrate], writer: &mut W) -> Result<()> {
let mut workbook = Workbook::new();
let properties = DocProperties::new().set_author("cargo-aprz");
workbook.set_properties(&properties);
let worksheet = workbook.add_worksheet().set_name("Crate Metrics")?;
let bold_format = Format::new().set_bold();
let category_format = Format::new()
.set_bold()
.set_background_color(Color::RGB(0x00FE_D7AA))
.set_align(FormatAlign::Left);
let left_align_format = Format::new().set_align(FormatAlign::Left);
let low_risk_format = Format::new()
.set_background_color(Color::RGB(0x00C8_E6C9))
.set_font_color(Color::RGB(0x002E_7D32))
.set_bold();
let medium_risk_format = Format::new()
.set_background_color(Color::RGB(0x00FF_F9C4))
.set_font_color(Color::RGB(0x00F5_7F17))
.set_bold();
let high_risk_format = Format::new()
.set_background_color(Color::RGB(0x00FF_CDD2))
.set_font_color(Color::RGB(0x00C6_2828))
.set_bold();
for (col_idx, crate_info) in crates.iter().enumerate() {
let header = format!("{} v{}", crate_info.name, crate_info.version);
#[expect(clippy::cast_possible_truncation, reason = "Column index limited by Excel's u16 column limit")]
worksheet.write_string_with_format(0, (col_idx + 1) as u16, &header, &bold_format)?;
}
worksheet.set_freeze_panes(1, 1)?;
let crate_metric_maps = common::build_metric_lookup_maps(crates);
let metrics_by_category = common::group_all_metrics_by_category(crates.iter().map(|c| c.metrics.as_slice()));
let mut row = 1;
let has_appraisals = crates.iter().any(|c| c.appraisal.is_some());
if has_appraisals {
worksheet.write_string_with_format(row, 0, "Appraisals", &bold_format)?;
for (col_idx, crate_info) in crates.iter().enumerate() {
if let Some(eval) = &crate_info.appraisal {
let value = common::format_appraisal_status(eval);
let format = match eval.risk {
Risk::Low => &low_risk_format,
Risk::Medium => &medium_risk_format,
Risk::High => &high_risk_format,
};
#[expect(clippy::cast_possible_truncation, reason = "Column index limited by Excel's u16 column limit")]
worksheet.write_string_with_format(row, (col_idx + 1) as u16, value, format)?;
}
}
row += 1;
worksheet.write_string_with_format(row, 0, "Reasons", &bold_format)?;
write_eval_row(worksheet, row, crates, |eval| common::join_with(
eval.expression_outcomes.iter().map(common::outcome_icon_name), "; "))?;
row += 1;
row += 1;
}
for category in MetricCategory::iter() {
if let Some(category_metric_names) = metrics_by_category.get(&category) {
worksheet.write_string_with_format(row, 0, category.as_uppercase_str(), &category_format)?;
#[expect(clippy::cast_possible_truncation, reason = "Column count is limited by Excel's u16 column limit")]
for c in 1..=crates.len() as u16 {
worksheet.write_blank(row, c, &category_format)?;
}
row += 1;
for &metric_name in category_metric_names {
worksheet.write_string(row, 0, metric_name)?;
for (col_idx, metric_map) in crate_metric_maps.iter().enumerate() {
if let Some(metric) = metric_map.get(metric_name)
&& let Some(ref value) = metric.value
{
#[expect(clippy::cast_possible_truncation, reason = "Column index limited by Excel's u16 column limit")]
write_metric_value(worksheet, row, (col_idx + 1) as u16, metric_name, value, &left_align_format)?;
}
}
row += 1;
}
row += 1;
}
}
worksheet.autofit();
let data = workbook.save_to_buffer()?;
writer.write_all(&data)?;
Ok(())
}
#[expect(unused_results, reason = "rust_xlsxwriter methods return &mut Worksheet for chaining")]
#[expect(clippy::cast_precision_loss, reason = "Intentional conversion to f64 for Excel output")]
fn write_metric_value(
worksheet: &mut rust_xlsxwriter::Worksheet,
row: u32,
col: u16,
metric_name: &str,
value: &MetricValue,
format: &Format,
) -> Result<()> {
match value {
MetricValue::UInt(u) => {
worksheet.write_number_with_format(row, col, *u as f64, format)?;
}
MetricValue::Float(f) => {
worksheet.write_number_with_format(row, col, *f, format)?;
}
MetricValue::Boolean(b) => {
worksheet.write_boolean_with_format(row, col, *b, format)?;
}
MetricValue::String(s) => {
if common::is_url(s.as_str()) {
worksheet.write_url(row, col, s.as_str())?;
}
else if common::is_keywords_metric(metric_name) || common::is_categories_metric(metric_name) {
let formatted = common::format_keywords_or_categories_with_prefix(s.as_str());
worksheet.write_string_with_format(row, col, formatted, format)?;
} else {
worksheet.write_string_with_format(row, col, s.as_str(), format)?;
}
}
MetricValue::DateTime(dt) => {
worksheet.write_string_with_format(row, col, dt.format("%Y-%m-%d").to_string(), format)?;
}
MetricValue::List(_) => {
let formatted = common::format_metric_value(value);
worksheet.write_string_with_format(row, col, formatted, format)?;
}
}
Ok(())
}
#[expect(unused_results, reason = "rust_xlsxwriter methods return &mut Worksheet for chaining")]
fn write_eval_row<F>(worksheet: &mut rust_xlsxwriter::Worksheet, row: u32, crates: &[ReportableCrate], extract_value: F) -> Result<()>
where
F: Fn(&Appraisal) -> String,
{
for (col_idx, crate_info) in crates.iter().enumerate() {
if let Some(eval) = &crate_info.appraisal {
let value = extract_value(eval);
#[expect(clippy::cast_possible_truncation, reason = "Column index limited by Excel's u16 column limit")]
worksheet.write_string(row, (col_idx + 1) as u16, &value)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::{ExpressionDisposition, ExpressionOutcome};
use crate::metrics::{Metric, MetricDef};
use std::sync::Arc;
static NAME_DEF: MetricDef = MetricDef {
name: "name",
description: "Crate name",
category: MetricCategory::Metadata,
extractor: |_| None,
default_value: || None,
};
static VERSION_DEF: MetricDef = MetricDef {
name: "version",
description: "Crate version",
category: MetricCategory::Metadata,
extractor: |_| None,
default_value: || None,
};
fn create_test_crate(name: &str, version: &str, evaluation: Option<Appraisal>) -> ReportableCrate {
let metrics = vec![
Metric::with_value(&NAME_DEF, MetricValue::String(name.into())),
Metric::with_value(&VERSION_DEF, MetricValue::String(version.into())),
];
ReportableCrate::new(name.into(), Arc::new(version.parse().unwrap()), metrics, evaluation)
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_empty_crates() {
let crates: Vec<ReportableCrate> = vec![];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_single_crate_no_evaluation() {
let crates = vec![create_test_crate("test_crate", "1.2.3", None)];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
assert_eq!(&output[0..2], b"PK");
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_single_crate_with_evaluation() {
let eval = Appraisal {
risk: Risk::Low,
expression_outcomes: vec![ExpressionOutcome::new("good".into(), "Good".into(), ExpressionDisposition::True)],
available_points: 1,
awarded_points: 1,
score: 100.0,
};
let crates = vec![create_test_crate("test_crate", "1.0.0", Some(eval))];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
assert_eq!(&output[0..2], b"PK");
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_multiple_crates() {
let crates = vec![
create_test_crate("crate_a", "1.0.0", None),
create_test_crate("crate_b", "2.0.0", None),
];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
assert_eq!(&output[0..2], b"PK");
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_denied_status() {
let eval = Appraisal {
risk: Risk::High,
expression_outcomes: vec![ExpressionOutcome::new("security".into(), "Security issue".into(), ExpressionDisposition::False)],
available_points: 1,
awarded_points: 0,
score: 0.0,
};
let crates = vec![create_test_crate("bad_crate", "1.0.0", Some(eval))];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_with_missing_data() {
let crates = vec![create_test_crate("missing", "1.0.0", None)];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot call GetSystemTimePreciseAsFileTime (rust_xlsxwriter)")]
fn test_generate_mixed_found_and_missing() {
let crates = vec![create_test_crate("good", "1.0.0", None), create_test_crate("bad", "1.0.0", None)];
let mut output = Vec::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(!output.is_empty());
}
}