stern4rust/reporting/package_roster.rs
1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5// Which rules ran against one package, and which did not.
6//
7// A run whose members answer to different rule sets cannot state one roster and
8// stay honest: `applied: <twenty-one rules>` is false the moment one package
9// applies twenty. But a run whose members all agree should not cost the reader a
10// block each either, which is what `agrees_with` is for -- the printer collapses
11// rosters that say the same thing and only separates the ones that do not.
12//
13// The name is what the comparison ignores. Two packages running the same rules
14// are one thing to report, and which package it was is the only difference.
15//
16// See [ADR-PerPackageConfiguration](../../docs/ADRs/ADR-PerPackageConfiguration.md).
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct PackageRoster {
19 pub package: String,
20 pub applied: Vec<String>,
21 pub skipped: Vec<String>,
22 pub unconfigured: Vec<(String, String)>,
23}
24
25impl PackageRoster {
26 pub fn new(
27 package: &str,
28 applied: Vec<String>,
29 skipped: Vec<String>,
30 unconfigured: Vec<(String, String)>,
31 ) -> Self {
32 Self {
33 package: package.to_string(),
34 applied,
35 skipped,
36 unconfigured,
37 }
38 }
39
40 pub fn agrees_with(&self, other: &Self) -> bool {
41 self.applied == other.applied
42 && self.skipped == other.skipped
43 && self.unconfigured == other.unconfigured
44 }
45
46 // Why a rule is missing, not merely that it is. A rule nobody asked for and
47 // a rule that could not run are different facts, and a reader who cannot
48 // tell them apart has no idea which one to act on.
49 pub fn absences(&self) -> Vec<String> {
50 self.skipped
51 .iter()
52 .map(|name| format!("{name} (skipped)"))
53 .chain(
54 self.unconfigured
55 .iter()
56 .map(|(name, requirement)| format!("{name} ({requirement})")),
57 )
58 .collect()
59 }
60}