Skip to main content

stern4rust/reporting/
rule_listing.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5use crate::reporting::output_format::OutputFormat;
6use crate::reporting::rule_explanation::RuleExplanation;
7use serde_json::Value;
8use serde_json::json;
9use serde_json::to_string_pretty;
10
11// The rule set as a document, in whichever form was asked for.
12//
13// This is not a report and it does not live on either printer, though it began
14// there. A printer holds what one run found -- files scanned, offences kept,
15// which rules were applied -- and renders that. A listing has no run behind it:
16// nothing was scanned, nothing was counted, and every field a printer carries
17// would be empty. The give-away was that both rendering functions took no
18// `self` and read no field; they were free functions wearing a printer's name.
19//
20// So the listing is its own subject, and it takes the format rather than the
21// caller choosing a printer by hand. That is also what keeps the two forms
22// honest with each other -- one type renders both, from one list, which is
23// ADR-MachineReadableReport's requirement that the two must not give different
24// pictures.
25pub struct RuleListing<'a> {
26    explanations: &'a [RuleExplanation],
27}
28
29impl<'a> RuleListing<'a> {
30    pub fn new(explanations: &'a [RuleExplanation]) -> Self {
31        Self { explanations }
32    }
33
34    pub fn render(&self, format: OutputFormat) -> String {
35        match format {
36            OutputFormat::Json => self.json(),
37            OutputFormat::Text => self.text(),
38        }
39    }
40
41    // One section per rule, in registry order, so the listing reads in the same
42    // order as the roster a report prints.
43    fn text(&self) -> String {
44        let mut out = String::from(
45            "stern4rust rules
46",
47        );
48        for entry in self.explanations {
49            out.push_str(&format!(
50                "
51{}
52  {}
53",
54                entry.name, entry.summary
55            ));
56            out.push_str(&Self::block("breaks", entry.breaks));
57            out.push_str(&Self::block("instead", entry.instead));
58        }
59        out
60    }
61
62    fn json(&self) -> String {
63        let entries: Vec<Value> = self
64            .explanations
65            .iter()
66            .map(|entry| {
67                json!({
68                    "name": entry.name,
69                    "summary": entry.summary,
70                    "breaks": entry.breaks,
71                    "instead": entry.instead,
72                })
73            })
74            .collect();
75        to_string_pretty(&json!({ "rules": entries }))
76            .unwrap_or_else(|_| String::from("{\"rules\":[]}"))
77    }
78
79    // Indented so a multi-line example stays one block rather than running into
80    // the next label.
81    fn block(label: &str, body: &str) -> String {
82        let mut out = format!(
83            "
84  {label}:
85"
86        );
87        for line in body.lines() {
88            out.push_str(&format!(
89                "      {line}
90"
91            ));
92        }
93        out
94    }
95}