Skip to main content

use_diagnostic_report/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use use_diagnostic_code::DiagnosticCode;
5use use_diagnostic_label::DiagnosticLabel;
6use use_diagnostic_level::DiagnosticLevel;
7use use_diagnostic_message::{DiagnosticMessage, DiagnosticNote};
8
9/// A structured diagnostic data value.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct Diagnostic {
12    code: Option<DiagnosticCode>,
13    level: DiagnosticLevel,
14    message: DiagnosticMessage,
15    labels: Vec<DiagnosticLabel>,
16    notes: Vec<DiagnosticNote>,
17}
18
19impl Diagnostic {
20    /// Creates a diagnostic with a level and message.
21    #[must_use]
22    pub const fn new(level: DiagnosticLevel, message: DiagnosticMessage) -> Self {
23        Self {
24            code: None,
25            level,
26            message,
27            labels: Vec::new(),
28            notes: Vec::new(),
29        }
30    }
31
32    /// Returns a diagnostic with the provided code attached.
33    #[must_use]
34    pub fn with_code(mut self, code: DiagnosticCode) -> Self {
35        self.code = Some(code);
36        self
37    }
38
39    /// Returns a diagnostic with the provided label appended.
40    #[must_use]
41    pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
42        self.labels.push(label);
43        self
44    }
45
46    /// Returns a diagnostic with the provided note appended.
47    #[must_use]
48    pub fn with_note(mut self, note: DiagnosticNote) -> Self {
49        self.notes.push(note);
50        self
51    }
52
53    /// Appends a label.
54    pub fn add_label(&mut self, label: DiagnosticLabel) {
55        self.labels.push(label);
56    }
57
58    /// Appends a note.
59    pub fn add_note(&mut self, note: DiagnosticNote) {
60        self.notes.push(note);
61    }
62
63    /// Returns the optional diagnostic code.
64    #[must_use]
65    pub const fn code(&self) -> Option<&DiagnosticCode> {
66        self.code.as_ref()
67    }
68
69    /// Returns the diagnostic level.
70    #[must_use]
71    pub const fn level(&self) -> DiagnosticLevel {
72        self.level
73    }
74
75    /// Returns the diagnostic message.
76    #[must_use]
77    pub const fn message(&self) -> &DiagnosticMessage {
78        &self.message
79    }
80
81    /// Returns the diagnostic labels.
82    #[must_use]
83    pub fn labels(&self) -> &[DiagnosticLabel] {
84        &self.labels
85    }
86
87    /// Returns the diagnostic notes.
88    #[must_use]
89    pub fn notes(&self) -> &[DiagnosticNote] {
90        &self.notes
91    }
92}
93
94/// An insertion-order collection of diagnostics.
95#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub struct DiagnosticReport {
97    diagnostics: Vec<Diagnostic>,
98}
99
100impl DiagnosticReport {
101    /// Creates an empty diagnostic report.
102    #[must_use]
103    pub const fn new() -> Self {
104        Self {
105            diagnostics: Vec::new(),
106        }
107    }
108
109    /// Adds a diagnostic to the end of the report.
110    pub fn add(&mut self, diagnostic: Diagnostic) {
111        self.diagnostics.push(diagnostic);
112    }
113
114    /// Returns the number of diagnostics in the report.
115    #[must_use]
116    pub const fn len(&self) -> usize {
117        self.diagnostics.len()
118    }
119
120    /// Returns `true` when the report contains no diagnostics.
121    #[must_use]
122    pub const fn is_empty(&self) -> bool {
123        self.diagnostics.is_empty()
124    }
125
126    /// Returns the diagnostics as a slice in insertion order.
127    #[must_use]
128    pub fn diagnostics(&self) -> &[Diagnostic] {
129        &self.diagnostics
130    }
131
132    /// Iterates diagnostics in insertion order.
133    pub fn iter(&self) -> core::slice::Iter<'_, Diagnostic> {
134        self.diagnostics.iter()
135    }
136
137    /// Counts diagnostics with the exact provided level.
138    #[must_use]
139    pub fn count_by_level(&self, level: DiagnosticLevel) -> usize {
140        self.diagnostics
141            .iter()
142            .filter(|diagnostic| diagnostic.level() == level)
143            .count()
144    }
145
146    /// Returns `true` when the report contains an error or fatal diagnostic.
147    #[must_use]
148    pub fn has_errors(&self) -> bool {
149        self.diagnostics
150            .iter()
151            .any(|diagnostic| diagnostic.level().is_error())
152    }
153
154    /// Returns `true` when the report contains a fatal diagnostic.
155    #[must_use]
156    pub fn has_fatal(&self) -> bool {
157        self.diagnostics
158            .iter()
159            .any(|diagnostic| diagnostic.level().is_fatal())
160    }
161
162    /// Returns the highest diagnostic severity in the report.
163    #[must_use]
164    pub fn highest_severity(&self) -> Option<DiagnosticLevel> {
165        self.diagnostics.iter().map(Diagnostic::level).max()
166    }
167
168    /// Extends this report with diagnostics from another report, preserving insertion order.
169    pub fn extend_report(&mut self, other: Self) {
170        self.diagnostics.extend(other.diagnostics);
171    }
172}
173
174impl FromIterator<Diagnostic> for DiagnosticReport {
175    fn from_iter<T: IntoIterator<Item = Diagnostic>>(diagnostics: T) -> Self {
176        Self {
177            diagnostics: diagnostics.into_iter().collect(),
178        }
179    }
180}
181
182impl<'a> IntoIterator for &'a DiagnosticReport {
183    type Item = &'a Diagnostic;
184    type IntoIter = core::slice::Iter<'a, Diagnostic>;
185
186    fn into_iter(self) -> Self::IntoIter {
187        self.iter()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::{Diagnostic, DiagnosticReport};
194    use use_diagnostic_code::DiagnosticCode;
195    use use_diagnostic_label::DiagnosticLabel;
196    use use_diagnostic_level::DiagnosticLevel;
197    use use_diagnostic_message::{DiagnosticMessage, DiagnosticNote};
198
199    fn message(text: &str) -> DiagnosticMessage {
200        DiagnosticMessage::new(text).expect("message should be valid")
201    }
202
203    #[test]
204    fn creates_diagnostic() {
205        let diagnostic = Diagnostic::new(DiagnosticLevel::Warning, message("check the value"));
206
207        assert_eq!(diagnostic.level(), DiagnosticLevel::Warning);
208        assert_eq!(diagnostic.message().as_str(), "check the value");
209        assert!(diagnostic.code().is_none());
210    }
211
212    #[test]
213    fn creates_diagnostic_with_code() {
214        let diagnostic = Diagnostic::new(DiagnosticLevel::Error, message("missing field"))
215            .with_code(
216                DiagnosticCode::new("VALIDATE_MISSING_FIELD").expect("code should be valid"),
217            );
218
219        assert_eq!(
220            diagnostic.code().map(DiagnosticCode::as_str),
221            Some("VALIDATE_MISSING_FIELD")
222        );
223    }
224
225    #[test]
226    fn creates_diagnostic_with_labels() {
227        let label = DiagnosticLabel::help(message("add the missing field"));
228        let diagnostic =
229            Diagnostic::new(DiagnosticLevel::Error, message("missing field")).with_label(label);
230
231        assert_eq!(diagnostic.labels().len(), 1);
232    }
233
234    #[test]
235    fn adds_and_iterates_report_diagnostics() {
236        let first = Diagnostic::new(DiagnosticLevel::Info, message("first"));
237        let second = Diagnostic::new(DiagnosticLevel::Warning, message("second"));
238        let mut report = DiagnosticReport::new();
239
240        report.add(first);
241        report.add(second);
242
243        let messages: Vec<&str> = report
244            .iter()
245            .map(|diagnostic| diagnostic.message().as_str())
246            .collect();
247
248        assert_eq!(messages, vec!["first", "second"]);
249    }
250
251    #[test]
252    fn counts_diagnostics_by_level() {
253        let report: DiagnosticReport = [
254            Diagnostic::new(DiagnosticLevel::Info, message("informational")),
255            Diagnostic::new(DiagnosticLevel::Error, message("error one")),
256            Diagnostic::new(DiagnosticLevel::Error, message("error two")),
257        ]
258        .into_iter()
259        .collect();
260
261        assert_eq!(report.count_by_level(DiagnosticLevel::Info), 1);
262        assert_eq!(report.count_by_level(DiagnosticLevel::Error), 2);
263    }
264
265    #[test]
266    fn detects_errors() {
267        let report: DiagnosticReport = [
268            Diagnostic::new(DiagnosticLevel::Warning, message("warning")),
269            Diagnostic::new(DiagnosticLevel::Error, message("error")),
270        ]
271        .into_iter()
272        .collect();
273
274        assert!(report.has_errors());
275    }
276
277    #[test]
278    fn detects_fatal_diagnostics() {
279        let report: DiagnosticReport = [
280            Diagnostic::new(DiagnosticLevel::Error, message("error")),
281            Diagnostic::new(DiagnosticLevel::Fatal, message("fatal")),
282        ]
283        .into_iter()
284        .collect();
285
286        assert!(report.has_fatal());
287    }
288
289    #[test]
290    fn returns_highest_severity() {
291        let report: DiagnosticReport = [
292            Diagnostic::new(DiagnosticLevel::Info, message("info")),
293            Diagnostic::new(DiagnosticLevel::Warning, message("warning")),
294            Diagnostic::new(DiagnosticLevel::Error, message("error")),
295        ]
296        .into_iter()
297        .collect();
298
299        assert_eq!(report.highest_severity(), Some(DiagnosticLevel::Error));
300    }
301
302    #[test]
303    fn extends_report_in_order() {
304        let mut report = DiagnosticReport::new();
305        report.add(Diagnostic::new(DiagnosticLevel::Info, message("first")));
306
307        let mut other = DiagnosticReport::new();
308        other.add(
309            Diagnostic::new(DiagnosticLevel::Warning, message("second"))
310                .with_note(DiagnosticNote::new("extra context").expect("note should be valid")),
311        );
312
313        report.extend_report(other);
314
315        let messages: Vec<&str> = report
316            .iter()
317            .map(|diagnostic| diagnostic.message().as_str())
318            .collect();
319
320        assert_eq!(messages, vec!["first", "second"]);
321    }
322}