use_diagnostic_report/
lib.rs1#![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#[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 #[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 #[must_use]
34 pub fn with_code(mut self, code: DiagnosticCode) -> Self {
35 self.code = Some(code);
36 self
37 }
38
39 #[must_use]
41 pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
42 self.labels.push(label);
43 self
44 }
45
46 #[must_use]
48 pub fn with_note(mut self, note: DiagnosticNote) -> Self {
49 self.notes.push(note);
50 self
51 }
52
53 pub fn add_label(&mut self, label: DiagnosticLabel) {
55 self.labels.push(label);
56 }
57
58 pub fn add_note(&mut self, note: DiagnosticNote) {
60 self.notes.push(note);
61 }
62
63 #[must_use]
65 pub const fn code(&self) -> Option<&DiagnosticCode> {
66 self.code.as_ref()
67 }
68
69 #[must_use]
71 pub const fn level(&self) -> DiagnosticLevel {
72 self.level
73 }
74
75 #[must_use]
77 pub const fn message(&self) -> &DiagnosticMessage {
78 &self.message
79 }
80
81 #[must_use]
83 pub fn labels(&self) -> &[DiagnosticLabel] {
84 &self.labels
85 }
86
87 #[must_use]
89 pub fn notes(&self) -> &[DiagnosticNote] {
90 &self.notes
91 }
92}
93
94#[derive(Clone, Debug, Default, Eq, PartialEq)]
96pub struct DiagnosticReport {
97 diagnostics: Vec<Diagnostic>,
98}
99
100impl DiagnosticReport {
101 #[must_use]
103 pub const fn new() -> Self {
104 Self {
105 diagnostics: Vec::new(),
106 }
107 }
108
109 pub fn add(&mut self, diagnostic: Diagnostic) {
111 self.diagnostics.push(diagnostic);
112 }
113
114 #[must_use]
116 pub const fn len(&self) -> usize {
117 self.diagnostics.len()
118 }
119
120 #[must_use]
122 pub const fn is_empty(&self) -> bool {
123 self.diagnostics.is_empty()
124 }
125
126 #[must_use]
128 pub fn diagnostics(&self) -> &[Diagnostic] {
129 &self.diagnostics
130 }
131
132 pub fn iter(&self) -> core::slice::Iter<'_, Diagnostic> {
134 self.diagnostics.iter()
135 }
136
137 #[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 #[must_use]
148 pub fn has_errors(&self) -> bool {
149 self.diagnostics
150 .iter()
151 .any(|diagnostic| diagnostic.level().is_error())
152 }
153
154 #[must_use]
156 pub fn has_fatal(&self) -> bool {
157 self.diagnostics
158 .iter()
159 .any(|diagnostic| diagnostic.level().is_fatal())
160 }
161
162 #[must_use]
164 pub fn highest_severity(&self) -> Option<DiagnosticLevel> {
165 self.diagnostics.iter().map(Diagnostic::level).max()
166 }
167
168 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}