use super::{ReportableCrate, common};
use crate::Result;
use crate::metrics::MetricValue;
use core::fmt::Write;
use serde_json::json;
#[expect(unused_results, reason = "HashMap::insert intentionally overwrites values")]
pub fn generate<W: Write>(crates: &[ReportableCrate], writer: &mut W) -> Result<()> {
let mut crate_data = Vec::new();
for crate_info in crates {
let mut crate_obj = serde_json::Map::new();
crate_obj.insert("name".to_string(), json!(crate_info.name));
crate_obj.insert("version".to_string(), json!(crate_info.version.to_string()));
if let Some(eval) = &crate_info.evaluation {
let mut eval_obj = serde_json::Map::new();
eval_obj.insert("result".to_string(), json!(common::format_acceptance_status(eval.accepted)));
eval_obj.insert("reasons".to_string(), json!(eval.reasons));
crate_obj.insert("evaluation".to_string(), json!(eval_obj));
}
let mut metrics_obj = serde_json::Map::new();
for metric in &crate_info.metrics {
if let Some(ref value) = metric.value {
let json_value = metric_value_to_json(value);
metrics_obj.insert(metric.name().to_string(), json_value);
}
}
crate_obj.insert("metrics".to_string(), json!(metrics_obj));
crate_data.push(json!(crate_obj));
}
let output = json!({
"crates": crate_data
});
write!(writer, "{}", serde_json::to_string_pretty(&output)?)?;
Ok(())
}
fn metric_value_to_json(value: &MetricValue) -> serde_json::Value {
match value {
MetricValue::UInt(u) => json!(u),
MetricValue::Float(f) => json!(f),
MetricValue::Boolean(b) => json!(b),
MetricValue::String(s) => json!(s.as_str()),
MetricValue::DateTime(dt) => json!(dt.format("%Y-%m-%d").to_string()),
MetricValue::List(values) => {
json!(values.iter().map(metric_value_to_json).collect::<Vec<_>>())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::EvaluationOutcome;
use crate::metrics::{Metric, MetricCategory, MetricDef};
use chrono::{DateTime, Utc};
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]
fn test_metric_value_to_json_float() {
let value = MetricValue::Float(1.234);
let json = metric_value_to_json(&value);
assert_eq!(json, json!(1.234));
}
#[test]
fn test_metric_value_to_json_boolean() {
let value = MetricValue::Boolean(true);
let json = metric_value_to_json(&value);
assert_eq!(json, json!(true));
}
#[test]
fn test_metric_value_to_json_text() {
let value = MetricValue::String("hello".into());
let json = metric_value_to_json(&value);
assert_eq!(json, json!("hello"));
}
#[test]
fn test_metric_value_to_json_datetime() {
let dt = DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z").unwrap();
let dt_utc: DateTime<Utc> = dt.into();
let value = MetricValue::DateTime(dt_utc);
let json = metric_value_to_json(&value);
assert!(json.as_str().unwrap().contains("2024-01-15"));
}
#[test]
fn test_generate_empty_crates() {
let crates: Vec<ReportableCrate> = vec![];
let mut output = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed["crates"].is_array());
assert_eq!(parsed["crates"].as_array().unwrap().len(), 0);
}
#[test]
fn test_generate_single_crate_no_ranking() {
let crates = vec![create_test_crate("test_crate", "1.2.3", None)];
let mut output = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["crates"][0]["name"], "test_crate");
assert_eq!(parsed["crates"][0]["version"], "1.2.3");
assert!(parsed["crates"][0]["evaluation"].is_null());
}
#[test]
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 = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["crates"][0]["evaluation"]["result"], "ACCEPTABLE");
assert_eq!(parsed["crates"][0]["evaluation"]["reasons"][0], "Good");
}
#[test]
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 = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["crates"].as_array().unwrap().len(), 2);
assert_eq!(parsed["crates"][0]["name"], "crate_a");
assert_eq!(parsed["crates"][1]["name"], "crate_b");
}
#[test]
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 = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["crates"][0]["evaluation"]["result"], "NOT ACCEPTABLE");
}
#[test]
fn test_generate_pretty_formatting() {
let crates = vec![create_test_crate("test", "1.0.0", None)];
let mut output = String::new();
let result = generate(&crates, &mut output);
result.unwrap();
assert!(output.contains('\n'));
assert!(output.contains(" "));
}
}