use serde::Serialize;
use crate::scoring::{Score, Scorecard};
use crate::types::{Category, Diagnostic};
#[derive(Debug, Serialize)]
pub struct Report<'a> {
pub version: u32,
pub diagnostics: &'a [Diagnostic],
pub summary: Summary,
pub score: Score,
pub category_scores: Vec<CategoryScoreEntry>,
}
#[derive(Debug, Serialize)]
pub struct CategoryScoreEntry {
pub category: String,
pub value: u32,
pub max: u32,
}
impl CategoryScoreEntry {
fn from_scorecard(scorecard: &Scorecard) -> Vec<Self> {
scorecard
.per_category
.iter()
.map(|cs| Self {
category: category_name(cs.category).to_string(),
value: cs.score.value,
max: cs.score.max,
})
.collect()
}
}
const fn category_name(c: Category) -> &'static str {
match c {
Category::Structure => "structure",
Category::Rhythm => "rhythm",
Category::Lexicon => "lexicon",
Category::Syntax => "syntax",
Category::Readability => "readability",
}
}
#[derive(Debug, Default, Serialize)]
pub struct Summary {
pub info: usize,
pub warning: usize,
pub error: usize,
pub total: usize,
}
impl Summary {
fn from_diagnostics(diagnostics: &[Diagnostic]) -> Self {
use crate::types::Severity;
let mut s = Self::default();
for d in diagnostics {
match d.severity {
Severity::Info => s.info += 1,
Severity::Warning => s.warning += 1,
Severity::Error => s.error += 1,
}
}
s.total = diagnostics.len();
s
}
}
pub const SCHEMA_VERSION: u32 = 2;
#[must_use]
pub fn render(diagnostics: &[Diagnostic], scorecard: &Scorecard) -> String {
let report = Report {
version: SCHEMA_VERSION,
diagnostics,
summary: Summary::from_diagnostics(diagnostics),
score: scorecard.global,
category_scores: CategoryScoreEntry::from_scorecard(scorecard),
};
serde_json::to_string_pretty(&report).unwrap_or_else(|_| "{}".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scoring::{self, ScoringConfig};
use crate::types::{Location, Severity, SourceFile};
fn sample_diag() -> Diagnostic {
Diagnostic::new(
"structure.sentence-too-long",
Severity::Warning,
Location::new(SourceFile::Anonymous, 3, 1, 42),
"Sentence is too long.",
)
}
fn scorecard(diags: &[Diagnostic]) -> Scorecard {
scoring::compute(diags, 1000, &ScoringConfig::default())
}
#[test]
fn render_is_valid_json() {
let diag = sample_diag();
let card = scorecard(std::slice::from_ref(&diag));
let json = render(&[diag], &card);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed.is_object());
assert_eq!(parsed["version"], SCHEMA_VERSION);
}
#[test]
fn render_includes_summary() {
let diag = sample_diag();
let card = scorecard(std::slice::from_ref(&diag));
let json = render(&[diag], &card);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["summary"]["warning"], 1);
assert_eq!(parsed["summary"]["info"], 0);
assert_eq!(parsed["summary"]["total"], 1);
}
#[test]
fn render_includes_score_and_categories() {
let diag = sample_diag();
let card = scorecard(std::slice::from_ref(&diag));
let json = render(&[diag], &card);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["score"]["value"].as_u64().is_some());
assert_eq!(parsed["score"]["max"], 100);
let cats = parsed["category_scores"].as_array().unwrap();
assert_eq!(cats.len(), 5);
assert_eq!(cats[0]["category"], "structure");
assert_eq!(cats[4]["category"], "readability");
}
#[test]
fn render_diagnostics_carry_weight() {
let diag = sample_diag();
let card = scorecard(std::slice::from_ref(&diag));
let json = render(&[diag], &card);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["diagnostics"][0]["weight"], 2);
}
#[test]
fn render_empty_diagnostics() {
let card = scorecard(&[]);
let json = render(&[], &card);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["summary"]["total"], 0);
assert!(parsed["diagnostics"].as_array().unwrap().is_empty());
}
#[test]
fn summary_counts_by_severity() {
let diagnostics = vec![
Diagnostic::new(
"a",
Severity::Info,
Location::new(SourceFile::Anonymous, 1, 1, 1),
"m1",
),
Diagnostic::new(
"a",
Severity::Warning,
Location::new(SourceFile::Anonymous, 1, 1, 1),
"m2",
),
Diagnostic::new(
"a",
Severity::Warning,
Location::new(SourceFile::Anonymous, 1, 1, 1),
"m3",
),
];
let summary = Summary::from_diagnostics(&diagnostics);
assert_eq!(summary.info, 1);
assert_eq!(summary.warning, 2);
assert_eq!(summary.total, 3);
}
}