Skip to main content

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}