1use serde::{Deserialize, Serialize};
4
5use crate::finding::Finding;
6use crate::outcome::{Outcome, Severity};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum NotRun {
16 CapabilityMissing,
19 Disabled,
21 NotApplicable,
23 Errored,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct RuleRun {
31 pub rule_id: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub viewport: Option<String>,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
43 pub wcag: Vec<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub not_run: Option<NotRun>,
47 #[serde(default)]
49 pub findings: usize,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub reason: Option<String>,
53}
54
55impl RuleRun {
56 pub fn ran(rule_id: impl Into<String>, findings: usize) -> Self {
57 RuleRun {
58 rule_id: rule_id.into(),
59 viewport: None,
60 wcag: Vec::new(),
61 not_run: None,
62 findings,
63 reason: None,
64 }
65 }
66
67 pub fn not_run(rule_id: impl Into<String>, why: NotRun) -> Self {
68 RuleRun {
69 rule_id: rule_id.into(),
70 viewport: None,
71 wcag: Vec::new(),
72 not_run: Some(why),
73 findings: 0,
74 reason: None,
75 }
76 }
77
78 pub fn in_viewport(mut self, v: impl Into<String>) -> Self {
80 self.viewport = Some(v.into());
81 self
82 }
83
84 pub fn with_wcag(mut self, criteria: impl IntoIterator<Item = impl Into<String>>) -> Self {
85 self.wcag = criteria.into_iter().map(Into::into).collect();
86 self
87 }
88
89 pub fn with_reason(mut self, r: impl Into<String>) -> Self {
90 self.reason = Some(r.into());
91 self
92 }
93
94 pub fn did_run(&self) -> bool {
95 self.not_run.is_none()
96 }
97}
98
99#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Summary {
104 pub fail: usize,
105 pub review: usize,
106 pub pass: usize,
107 pub untested: usize,
108 pub critical: usize,
109 pub high: usize,
110 pub medium: usize,
111 pub low: usize,
112 pub rules_not_run: usize,
114}
115
116impl Summary {
117 pub fn problems(&self) -> usize {
120 self.fail + self.review
121 }
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
126pub struct Report {
127 pub findings: Vec<Finding>,
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
129 pub rule_runs: Vec<RuleRun>,
130 pub summary: Summary,
131}
132
133impl Report {
134 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn push(&mut self, f: Finding) {
139 self.findings.push(f);
140 }
141
142 pub fn extend(&mut self, fs: impl IntoIterator<Item = Finding>) {
143 self.findings.extend(fs);
144 }
145
146 pub fn record(&mut self, r: RuleRun) {
147 self.rule_runs.push(r);
148 }
149
150 pub fn by_outcome(&self, o: Outcome) -> impl Iterator<Item = &Finding> {
152 self.findings.iter().filter(move |f| f.outcome == o)
153 }
154
155 pub fn visible(&self) -> impl Iterator<Item = &Finding> {
157 self.findings
158 .iter()
159 .filter(|f| f.outcome.is_visible_by_default())
160 }
161
162 pub fn finish(mut self) -> Self {
165 let mut s = Summary::default();
166 for f in &self.findings {
167 match f.outcome {
168 Outcome::Fail => s.fail += 1,
169 Outcome::Review => s.review += 1,
170 Outcome::Pass => s.pass += 1,
171 Outcome::Untested => s.untested += 1,
172 }
173 if f.outcome.is_problem() {
176 match f.severity {
177 Severity::Critical => s.critical += 1,
178 Severity::High => s.high += 1,
179 Severity::Medium => s.medium += 1,
180 Severity::Low => s.low += 1,
181 }
182 }
183 }
184 s.rules_not_run = self.rule_runs.iter().filter(|r| !r.did_run()).count();
185 self.summary = s;
186 self
187 }
188}