1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
//! Validation findings. [`Severity::Fatal`] fails [`Report::ok`]; [`Severity::Warning`] does not.
use crate::bt::Path;
use std::fmt;
/// Finding severity. Only [`Severity::Fatal`] fails [`Report::ok`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
/// Fails [`Report::ok`].
Fatal,
/// Does not fail [`Report::ok`].
Warning,
/// Does not fail [`Report::ok`].
Info,
}
/// Provenance of a registered rule id.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
/// Standard text and artefacts agree.
Both,
/// Standard text only.
StandardOnly,
/// Artefact-only (eval may be a no-op).
ArtefactOnly,
/// Crate-owned id (`CORE-*`, `PINT-TAX`, `IBR-*-MY`).
Crate,
}
/// One rule hit at a [`Path`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
/// Registered rule id (`BR-02`, `PINT-TAX`).
pub id: &'static str,
/// Fatal fails [`Report::ok`]; Warning and Info do not.
pub severity: Severity,
/// Finding location ([`Path`]; index is 0-based).
pub path: Path,
/// Authority wording. Arithmetic expected/actual belongs in [`Self::detail`].
pub message: String,
/// Optional expected/actual. Not appended to [`Self::message`] by constructors.
pub detail: Option<String>,
/// Optional hint. Not shown by `Display`.
pub hint: Option<String>,
}
impl Finding {
/// Fatal finding. Fails [`Report::ok`].
pub fn fatal(id: &'static str, path: Path, message: impl Into<String>) -> Self {
Self {
id,
severity: Severity::Fatal,
path,
message: message.into(),
detail: None,
hint: None,
}
}
/// Warning finding. Does not fail [`Report::ok`].
pub fn warning(id: &'static str, path: Path, message: impl Into<String>) -> Self {
Self {
id,
severity: Severity::Warning,
path,
message: message.into(),
detail: None,
hint: None,
}
}
/// Info finding. Does not fail [`Report::ok`].
pub fn info(id: &'static str, path: Path, message: impl Into<String>) -> Self {
Self {
id,
severity: Severity::Info,
path,
message: message.into(),
detail: None,
hint: None,
}
}
}
/// Validation result. [`Self::ok`] is “no Fatal”, not “no findings”.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Report {
/// Collected hits, unsorted until [`Self::sort_stable`].
pub findings: Vec<Finding>,
/// Profile slug of the invoice that was checked.
pub profile_slug: &'static str,
/// Number of rule evals invoked (CORE + extras).
pub rules_checked: usize,
}
impl Report {
/// `true` when no finding is [`Severity::Fatal`]. Warning and Info do not fail.
pub fn ok(&self) -> bool {
!self.findings.iter().any(|f| f.severity == Severity::Fatal)
}
/// Append a finding. Does not sort.
pub fn push(&mut self, finding: Finding) {
self.findings.push(finding);
}
/// Sort by severity, then path display, then id.
pub fn sort_stable(&mut self) {
self.findings.sort_by(|a, b| {
a.severity
.cmp(&b.severity)
.then_with(|| a.path.to_string().cmp(&b.path.to_string()))
.then_with(|| a.id.cmp(b.id))
});
}
}
impl fmt::Display for Finding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {} — {}", self.id, self.path, self.message)?;
if let Some(d) = &self.detail {
write!(f, " ({d})")?;
}
Ok(())
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.ok() {
return write!(f, "valid");
}
for finding in &self.findings {
writeln!(f, "{finding}")?;
}
Ok(())
}
}