use super::{ReportableCrate, common};
use crate::Result;
use crate::expr::EvaluationOutcome;
use crate::metrics::{Metric, 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 acceptable_format = Format::new()
.set_background_color(Color::RGB(0x00C8_E6C9))
.set_font_color(Color::RGB(0x002E_7D32))
.set_bold();
let not_acceptable_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_metrics: Vec<&[Metric]> = crates.iter().map(|c| c.metrics.as_slice()).collect();
let metrics_by_category = common::group_all_metrics_by_category(&crate_metrics);
let mut row = 1;
let has_evaluations = crates.iter().any(|c| c.evaluation.is_some());
if has_evaluations {
worksheet.write_string_with_format(row, 0, "Evaluation Result", &bold_format)?;
for (col_idx, crate_info) in crates.iter().enumerate() {
if let Some(eval) = &crate_info.evaluation {
let value = common::format_acceptance_status(eval.accepted);
let format = if eval.accepted {
&acceptable_format
} else {
¬_acceptable_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| eval.reasons.join("; "))?;
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, format!("{category}").to_uppercase(), &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, metrics) in crate_metrics.iter().enumerate() {
if let Some(metric) = metrics.iter().find(|m| m.name() == metric_name.as_str())
&& 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(&EvaluationOutcome) -> String,
{
for (col_idx, crate_info) in crates.iter().enumerate() {
if let Some(eval) = &crate_info.evaluation {
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::metrics::MetricDef;
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<EvaluationOutcome>) -> 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.to_string(), 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_ranking() {
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_ranking() {
let eval = EvaluationOutcome {
accepted: true,
reasons: vec!["Good".to_string()],
};
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 = EvaluationOutcome {
accepted: false,
reasons: vec!["Security issue".to_string()],
};
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());
}
}