Skip to main content

a11y_report/
finding.rs

1//! Ein einzelnes Prüfergebnis.
2
3use serde::{Deserialize, Serialize};
4
5use crate::extra::Extra;
6use crate::outcome::{Outcome, Severity, WcagLevel};
7
8/// Wo ein Befund sitzt. Die drei Oberflächen verorten unterschiedlich:
9/// astro-post-audit über Dateipfade, auditmysite über AXTree-Knoten, LiveAudit
10/// über den Index in seiner Arena. Alle Felder sind deshalb optional — welche
11/// gefüllt sind, hängt vom Host ab, nicht von der Regel.
12#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Location {
14    /// Datei relativ zur Ausgabewurzel, z. B. `about/index.html`.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub file: Option<String>,
17    /// URL der geprüften Seite.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub url: Option<String>,
20    /// CSS-Selektor zum Wiederfinden des Elements.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub selector: Option<String>,
23    /// Knotenkennung des Hosts — AXTree-Backend-ID oder Arena-Index als Text.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub node: Option<String>,
26    /// Hinweis auf die Quelldatei, wenn der Host sie zurückrechnen kann.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub source_hint: Option<String>,
29}
30
31impl Location {
32    pub fn file(path: impl Into<String>) -> Self {
33        Location {
34            file: Some(path.into()),
35            ..Default::default()
36        }
37    }
38
39    pub fn node(id: impl Into<String>) -> Self {
40        Location {
41            node: Some(id.into()),
42            ..Default::default()
43        }
44    }
45
46    pub fn with_selector(mut self, s: impl Into<String>) -> Self {
47        self.selector = Some(s.into());
48        self
49    }
50
51    pub fn with_url(mut self, u: impl Into<String>) -> Self {
52        self.url = Some(u.into());
53        self
54    }
55
56    /// Nichts gesetzt — der Befund gilt dem Dokument als Ganzem.
57    pub fn is_empty(&self) -> bool {
58        self == &Location::default()
59    }
60}
61
62/// Woher ein Befund seine Tatsachenbasis hat. Macht nachvollziehbar, ob eine
63/// Aussage aus dem nativen Accessibility-Tree stammt, aus einem Attribut oder
64/// aus einer Messung — was je nach Oberfläche unterschiedlich verfügbar ist.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct Evidence {
67    /// `ax_tree`, `dom_attribute`, `meta`, `css_property`, `http_header`, `computed`.
68    pub source: String,
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub field: Option<String>,
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub value: Option<String>,
73}
74
75impl Evidence {
76    pub fn new(source: impl Into<String>) -> Self {
77        Evidence {
78            source: source.into(),
79            field: None,
80            value: None,
81        }
82    }
83
84    pub fn ax_tree(value: impl Into<String>) -> Self {
85        Evidence {
86            source: "ax_tree".into(),
87            field: None,
88            value: Some(value.into()),
89        }
90    }
91
92    pub fn dom_attribute(field: impl Into<String>, value: Option<String>) -> Self {
93        Evidence {
94            source: "dom_attribute".into(),
95            field: Some(field.into()),
96            value,
97        }
98    }
99
100    /// Aus einer Messung abgeleitet statt direkt abgelesen, z. B. ein Kontrastwert.
101    pub fn computed(field: impl Into<String>, value: impl Into<String>) -> Self {
102        Evidence {
103            source: "computed".into(),
104            field: Some(field.into()),
105            value: Some(value.into()),
106        }
107    }
108}
109
110/// Ein Prüfergebnis.
111///
112/// Wird über [`Finding::fail`], [`Finding::review`], [`Finding::pass`] oder
113/// [`Finding::untested`] erzeugt und mit den `with_*`-Methoden angereichert —
114/// der Zustand ist damit immer bewusst gesetzt und nie ein Vorgabewert.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct Finding {
117    /// Stabile Regelkennung, z. B. `a11y/img-alt`. Über alle Oberflächen
118    /// identisch — das ist der Sinn des gemeinsamen Modells.
119    pub rule_id: String,
120    /// Menschenlesbarer Name der Regel, für Berichte, die mehr als die Kennung
121    /// zeigen wollen.
122    #[serde(default, skip_serializing_if = "Option::is_none")]
123    pub rule_name: Option<String>,
124    pub outcome: Outcome,
125    pub severity: Severity,
126    /// Was ist der Fall. Sachlich, ohne Handlungsanweisung.
127    pub message: String,
128
129    #[serde(default, skip_serializing_if = "Location::is_empty")]
130    pub location: Location,
131
132    /// Die berechnete Rolle des betroffenen Elements.
133    ///
134    /// Gehört zum Befund, nicht zur Verortung: Wer einen Bericht liest, will
135    /// wissen, *was* das Element für die Assistenztechnik war — ein Selektor
136    /// allein sagt das nicht. Leer, wenn der Host keine Semantik liefert.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub role: Option<String>,
139    /// Der Accessible Name des betroffenen Elements, soweit vorhanden.
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub name: Option<String>,
142
143    /// Erfüllte bzw. verletzte WCAG-Erfolgskriterien, z. B. `["1.1.1"]`.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub wcag: Vec<String>,
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub wcag_level: Option<WcagLevel>,
148    /// Freie Schlagworte, z. B. `["best-practice"]` oder `en301549:9.1.1.1`.
149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
150    pub tags: Vec<String>,
151
152    /// Was zu tun ist.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub help: Option<String>,
155    #[serde(default, skip_serializing_if = "Option::is_none")]
156    pub help_url: Option<String>,
157    /// Konkreter Vorschlag in Prosa.
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub suggestion: Option<String>,
160    /// Konkreter Vorschlag als Code.
161    #[serde(default, skip_serializing_if = "Option::is_none")]
162    pub suggested_code: Option<String>,
163
164    /// Das betroffene Markup, soweit der Host es liefern kann.
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    pub snippet: Option<String>,
167    #[serde(default, skip_serializing_if = "Vec::is_empty")]
168    pub evidence: Vec<Evidence>,
169
170    /// Werkzeugspezifische Beigabe. Nie serialisiert, nicht Teil der Identität
171    /// dieses Befunds — siehe [`Extra`].
172    #[serde(skip)]
173    pub extra: Extra,
174}
175
176impl Finding {
177    fn new(outcome: Outcome, rule_id: impl Into<String>, message: impl Into<String>) -> Self {
178        Finding {
179            rule_id: rule_id.into(),
180            rule_name: None,
181            outcome,
182            severity: Severity::default(),
183            message: message.into(),
184            location: Location::default(),
185            role: None,
186            name: None,
187            wcag: Vec::new(),
188            wcag_level: None,
189            tags: Vec::new(),
190            help: None,
191            help_url: None,
192            suggestion: None,
193            suggested_code: None,
194            snippet: None,
195            evidence: Vec::new(),
196            extra: Extra::none(),
197        }
198    }
199
200    /// Automatisch festgestelltes Problem.
201    pub fn fail(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
202        Self::new(Outcome::Fail, rule_id, message)
203    }
204
205    /// Heuristischer Verdacht — braucht menschliche Bestätigung.
206    pub fn review(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
207        Self::new(Outcome::Review, rule_id, message)
208    }
209
210    /// Automatische Prüfung bestanden.
211    pub fn pass(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
212        Self::new(Outcome::Pass, rule_id, message)
213    }
214
215    /// Automatisiert nicht beurteilbar — erzeugt einen Punkt auf der manuellen
216    /// Prüfliste, kein Urteil.
217    pub fn untested(rule_id: impl Into<String>, message: impl Into<String>) -> Self {
218        Self::new(Outcome::Untested, rule_id, message)
219    }
220
221    pub fn with_severity(mut self, s: Severity) -> Self {
222        self.severity = s;
223        self
224    }
225
226    pub fn with_rule_name(mut self, n: impl Into<String>) -> Self {
227        self.rule_name = Some(n.into());
228        self
229    }
230
231    /// Rolle und Accessible Name des betroffenen Elements.
232    pub fn with_element(mut self, role: Option<String>, name: Option<String>) -> Self {
233        self.role = role;
234        self.name = name;
235        self
236    }
237
238    /// Hängt eine werkzeugspezifische Beigabe an. Siehe [`Extra`].
239    pub fn with_extra<T: std::any::Any + Send + Sync>(mut self, value: T) -> Self {
240        self.extra = Extra::new(value);
241        self
242    }
243
244    pub fn at(mut self, l: Location) -> Self {
245        self.location = l;
246        self
247    }
248
249    pub fn with_wcag(mut self, criteria: impl IntoIterator<Item = impl Into<String>>) -> Self {
250        self.wcag = criteria.into_iter().map(Into::into).collect();
251        self
252    }
253
254    pub fn with_wcag_level(mut self, l: WcagLevel) -> Self {
255        self.wcag_level = Some(l);
256        self
257    }
258
259    pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
260        self.tags = tags.into_iter().map(Into::into).collect();
261        self
262    }
263
264    pub fn with_help(mut self, help: impl Into<String>) -> Self {
265        self.help = Some(help.into());
266        self
267    }
268
269    pub fn with_help_url(mut self, url: impl Into<String>) -> Self {
270        self.help_url = Some(url.into());
271        self
272    }
273
274    pub fn with_suggestion(mut self, s: impl Into<String>) -> Self {
275        self.suggestion = Some(s.into());
276        self
277    }
278
279    pub fn with_suggested_code(mut self, s: impl Into<String>) -> Self {
280        self.suggested_code = Some(s.into());
281        self
282    }
283
284    pub fn with_snippet(mut self, s: impl Into<String>) -> Self {
285        self.snippet = Some(s.into());
286        self
287    }
288
289    pub fn with_evidence(mut self, e: impl IntoIterator<Item = Evidence>) -> Self {
290        self.evidence = e.into_iter().collect();
291        self
292    }
293
294    pub fn add_evidence(mut self, e: Evidence) -> Self {
295        self.evidence.push(e);
296        self
297    }
298}