use std::collections::BTreeMap;
use zuit_core::analyzer::Dimension;
use zuit_core::engine::Report;
use serde::Serialize;
use crate::ReportError;
const V1_DIMENSION_ORDER: &[Dimension] = &[
Dimension::Maintainability,
Dimension::Security,
Dimension::Complexity,
Dimension::Documentation,
Dimension::TestSmell,
];
#[derive(Serialize)]
struct ToolInfo {
name: &'static str,
version: &'static str,
}
fn score_to_grade(score: f32) -> &'static str {
if score >= 90.0 {
"A"
} else if score >= 80.0 {
"B"
} else if score >= 70.0 {
"C"
} else if score >= 60.0 {
"D"
} else {
"F"
}
}
struct OrderedScores(Vec<(String, f32)>);
impl Serialize for OrderedScores {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = s.serialize_map(Some(self.0.len()))?;
for (k, v) in &self.0 {
map.serialize_entry(k, v)?;
}
map.end()
}
}
struct OrderedGrades(Vec<(String, &'static str)>);
impl Serialize for OrderedGrades {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = s.serialize_map(Some(self.0.len()))?;
for (k, v) in &self.0 {
map.serialize_entry(k, v)?;
}
map.end()
}
}
#[derive(Serialize)]
struct JsonReport<'a> {
schema_version: u32,
tool: ToolInfo,
scores: OrderedScores,
grades: OrderedGrades,
findings: &'a Vec<zuit_core::finding::Finding>,
stats: &'a zuit_core::engine::RunStats,
}
pub fn render_json(report: &Report) -> Result<String, ReportError> {
let mut score_pairs: Vec<(String, f32)> = Vec::with_capacity(report.scores.len());
let mut grade_pairs: Vec<(String, &'static str)> = Vec::with_capacity(report.scores.len());
for dim in V1_DIMENSION_ORDER {
if let Some(score) = report.scores.get(dim) {
let key = dim.to_string();
let value = score.value();
grade_pairs.push((key.clone(), score_to_grade(value)));
score_pairs.push((key, value));
}
}
let custom_scores: BTreeMap<String, f32> = report
.scores
.iter()
.filter(|(dim, _)| !V1_DIMENSION_ORDER.contains(dim))
.map(|(dim, s)| (dim.to_string(), s.value()))
.collect();
for (key, value) in &custom_scores {
grade_pairs.push((key.clone(), score_to_grade(*value)));
}
score_pairs.extend(custom_scores);
let json_report = JsonReport {
schema_version: report.schema_version,
tool: ToolInfo {
name: "zuit",
version: env!("CARGO_PKG_VERSION"),
},
scores: OrderedScores(score_pairs),
grades: OrderedGrades(grade_pairs),
findings: &report.findings,
stats: &report.stats,
};
Ok(serde_json::to_string_pretty(&json_report)?)
}
#[cfg(test)]
mod tests {
use super::*;
use zuit_core::engine::RunStats;
use serde_json::Value;
fn empty_report() -> Report {
use zuit_core::analyzer::Dimension;
use zuit_core::score::aggregate_dimension_score;
use std::collections::BTreeMap;
let mut scores = BTreeMap::new();
for dim in [
Dimension::Maintainability,
Dimension::Security,
Dimension::Complexity,
Dimension::Documentation,
Dimension::TestSmell,
] {
scores.insert(dim, aggregate_dimension_score(&[], 1.0));
}
Report {
schema_version: 1,
findings: vec![],
scores,
stats: RunStats {
files_scanned: 0,
parse_failures: 0,
elapsed_ms: 0,
suppressed: 0,
cache_hits: 0,
},
}
}
#[test]
fn top_level_field_order() {
let report = empty_report();
let json = render_json(&report).unwrap();
let v: serde_json::Map<String, Value> = serde_json::from_str(&json).unwrap();
let keys: Vec<&str> = v.keys().map(String::as_str).collect();
assert_eq!(
keys,
&[
"schema_version",
"tool",
"scores",
"grades",
"findings",
"stats"
]
);
}
#[test]
fn tool_block_present() {
let report = empty_report();
let json = render_json(&report).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["tool"]["name"], "zuit");
assert!(v["tool"]["version"].is_string());
}
#[test]
fn schema_version_is_1() {
let report = empty_report();
let json = render_json(&report).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["schema_version"], 1);
}
#[test]
fn score_order_is_stable() {
let report = empty_report();
let json = render_json(&report).unwrap();
let v: serde_json::Map<String, Value> = serde_json::from_str(&json).unwrap();
let score_keys: Vec<&str> = v["scores"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(score_keys[0], "maintainability");
assert_eq!(score_keys[1], "security");
assert_eq!(score_keys[2], "complexity");
assert_eq!(score_keys[3], "documentation");
assert_eq!(score_keys[4], "test_smell");
}
#[test]
fn grades_block_present_and_ordered() {
let report = empty_report();
let json = render_json(&report).unwrap();
let v: serde_json::Map<String, Value> = serde_json::from_str(&json).unwrap();
let grade_keys: Vec<&str> = v["grades"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert_eq!(grade_keys[0], "maintainability");
assert_eq!(grade_keys[1], "security");
assert_eq!(grade_keys[2], "complexity");
assert_eq!(grade_keys[3], "documentation");
assert_eq!(grade_keys[4], "test_smell");
}
#[test]
fn grade_boundaries() {
assert_eq!(score_to_grade(100.0), "A");
assert_eq!(score_to_grade(90.0), "A");
assert_eq!(score_to_grade(89.99), "B");
assert_eq!(score_to_grade(80.0), "B");
assert_eq!(score_to_grade(70.0), "C");
assert_eq!(score_to_grade(60.0), "D");
assert_eq!(score_to_grade(0.0), "F");
}
#[test]
fn grades_values_match_score_to_grade() {
use zuit_core::analyzer::Dimension;
use zuit_core::score::Score;
use std::collections::BTreeMap;
let mut scores: BTreeMap<Dimension, Score> = BTreeMap::new();
scores.insert(Dimension::Security, Score(92.0));
scores.insert(Dimension::Maintainability, Score(85.0));
scores.insert(Dimension::Complexity, Score(75.0));
scores.insert(Dimension::Documentation, Score(60.0));
scores.insert(Dimension::TestSmell, Score(55.0));
let report = Report {
schema_version: 1,
findings: vec![],
scores,
stats: zuit_core::engine::RunStats {
files_scanned: 0,
parse_failures: 0,
elapsed_ms: 0,
suppressed: 0,
cache_hits: 0,
},
};
let json = render_json(&report).unwrap();
let v: Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["grades"]["security"], "A"); assert_eq!(v["grades"]["maintainability"], "B"); assert_eq!(v["grades"]["complexity"], "C"); assert_eq!(v["grades"]["documentation"], "D"); assert_eq!(v["grades"]["test_smell"], "F"); }
}