stern4rust/json_printer.rs
1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use std::collections::BTreeSet;
6
7use serde_json::json;
8
9use crate::offence::Offence;
10use crate::offence_threshold::OffenceThreshold;
11
12// The same run as data rather than as a table.
13//
14// The table is sized to its contents and meant for a person. Nothing can parse
15// it reliably -- paths and descriptions both contain spaces, and descriptions
16// carry backticks, quotes and semicolons, so splitting on whitespace is
17// guesswork. A gate script or an agent reads this instead and never has to
18// infer where one column ends and the next begins.
19pub struct JsonPrinter {
20 files_scanned: usize,
21 threshold: OffenceThreshold,
22}
23
24impl JsonPrinter {
25 pub fn new(files_scanned: usize) -> Self {
26 Self {
27 files_scanned,
28 threshold: OffenceThreshold::default(),
29 }
30 }
31
32 pub fn with_threshold(self, threshold: OffenceThreshold) -> Self {
33 Self { threshold, ..self }
34 }
35
36 // Returns the document rather than printing it, so the shape is assertable
37 // in a test instead of being checked for not panicking.
38 // offences_found is the true total and offences is what survived the
39 // threshold, so a consumer reading only the array can still see that it is
40 // not the whole story. rules_broken counts every rule that was broken, not
41 // only those whose offences fitted.
42 pub fn render(&self, offences: &[Offence]) -> String {
43 let broken: BTreeSet<&str> = offences.iter().map(|offence| offence.rule).collect();
44 let shown = self.threshold.kept(offences);
45 let document = json!({
46 "files_scanned": self.files_scanned,
47 "offences_found": offences.len(),
48 "offences_reported": shown.len(),
49 "offences_omitted": self.threshold.omitted(offences),
50 "offence_threshold": self.threshold.limit(),
51 "rules_broken": broken.len(),
52 "offences": shown,
53 });
54 serde_json::to_string_pretty(&document).unwrap_or_default()
55 }
56
57 pub fn print(&self, offences: &[Offence]) {
58 println!("{}", self.render(offences));
59 }
60}