miette/
miette_diagnostic.rs

1use std::{
2    error::Error,
3    fmt::{Debug, Display},
4};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9use crate::{Diagnostic, LabeledSpan, Severity};
10
11/// Diagnostic that can be created at runtime.
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct MietteDiagnostic {
15    /// Displayed diagnostic message
16    pub message: String,
17    /// Unique diagnostic code to look up more information
18    /// about this Diagnostic. Ideally also globally unique, and documented
19    /// in the toplevel crate's documentation for easy searching.
20    /// Rust path format (`foo::bar::baz`) is recommended, but more classic
21    /// codes like `E0123` will work just fine
22    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
23    pub code: Option<String>,
24    /// [`Diagnostic`] severity. Intended to be used by
25    /// [`ReportHandler`](crate::ReportHandler)s to change the way different
26    /// [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]
27    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
28    pub severity: Option<Severity>,
29    /// Additional help text related to this Diagnostic
30    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
31    pub help: Option<String>,
32    /// URL to visit for a more detailed explanation/help about this
33    /// [`Diagnostic`].
34    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
35    pub url: Option<String>,
36    /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`]
37    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
38    pub labels: Option<Vec<LabeledSpan>>,
39}
40
41impl Display for MietteDiagnostic {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}", &self.message)
44    }
45}
46
47impl Error for MietteDiagnostic {}
48
49impl Diagnostic for MietteDiagnostic {
50    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
51        self.code
52            .as_ref()
53            .map(Box::new)
54            .map(|c| c as Box<dyn Display>)
55    }
56
57    fn severity(&self) -> Option<Severity> {
58        self.severity
59    }
60
61    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
62        self.help
63            .as_ref()
64            .map(Box::new)
65            .map(|c| c as Box<dyn Display>)
66    }
67
68    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
69        self.url
70            .as_ref()
71            .map(Box::new)
72            .map(|c| c as Box<dyn Display>)
73    }
74
75    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
76        self.labels
77            .as_ref()
78            .map(|ls| ls.iter().cloned())
79            .map(Box::new)
80            .map(|b| b as Box<dyn Iterator<Item = LabeledSpan>>)
81    }
82}
83
84impl MietteDiagnostic {
85    /// Create a new dynamic diagnostic with the given message.
86    ///
87    /// # Examples
88    /// ```
89    /// use miette::{Diagnostic, MietteDiagnostic, Severity};
90    ///
91    /// let diag = MietteDiagnostic::new("Oops, something went wrong!");
92    /// assert_eq!(diag.to_string(), "Oops, something went wrong!");
93    /// assert_eq!(diag.message, "Oops, something went wrong!");
94    /// ```
95    pub fn new(message: impl Into<String>) -> Self {
96        Self {
97            message: message.into(),
98            labels: None,
99            severity: None,
100            code: None,
101            help: None,
102            url: None,
103        }
104    }
105
106    /// Return new diagnostic with the given code.
107    ///
108    /// # Examples
109    /// ```
110    /// use miette::{Diagnostic, MietteDiagnostic};
111    ///
112    /// let diag = MietteDiagnostic::new("Oops, something went wrong!").with_code("foo::bar::baz");
113    /// assert_eq!(diag.message, "Oops, something went wrong!");
114    /// assert_eq!(diag.code, Some("foo::bar::baz".to_string()));
115    /// ```
116    pub fn with_code(mut self, code: impl Into<String>) -> Self {
117        self.code = Some(code.into());
118        self
119    }
120
121    /// Return new diagnostic with the given severity.
122    ///
123    /// # Examples
124    /// ```
125    /// use miette::{Diagnostic, MietteDiagnostic, Severity};
126    ///
127    /// let diag = MietteDiagnostic::new("I warn you to stop!").with_severity(Severity::Warning);
128    /// assert_eq!(diag.message, "I warn you to stop!");
129    /// assert_eq!(diag.severity, Some(Severity::Warning));
130    /// ```
131    pub fn with_severity(mut self, severity: Severity) -> Self {
132        self.severity = Some(severity);
133        self
134    }
135
136    /// Return new diagnostic with the given help message.
137    ///
138    /// # Examples
139    /// ```
140    /// use miette::{Diagnostic, MietteDiagnostic};
141    ///
142    /// let diag = MietteDiagnostic::new("PC is not working").with_help("Try to reboot it again");
143    /// assert_eq!(diag.message, "PC is not working");
144    /// assert_eq!(diag.help, Some("Try to reboot it again".to_string()));
145    /// ```
146    pub fn with_help(mut self, help: impl Into<String>) -> Self {
147        self.help = Some(help.into());
148        self
149    }
150
151    /// Return new diagnostic with the given URL.
152    ///
153    /// # Examples
154    /// ```
155    /// use miette::{Diagnostic, MietteDiagnostic};
156    ///
157    /// let diag = MietteDiagnostic::new("PC is not working")
158    ///     .with_url("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work");
159    /// assert_eq!(diag.message, "PC is not working");
160    /// assert_eq!(
161    ///     diag.url,
162    ///     Some("https://letmegooglethat.com/?q=Why+my+pc+doesn%27t+work".to_string())
163    /// );
164    /// ```
165    pub fn with_url(mut self, url: impl Into<String>) -> Self {
166        self.url = Some(url.into());
167        self
168    }
169
170    /// Return new diagnostic with the given label.
171    ///
172    /// Discards previous labels
173    ///
174    /// # Examples
175    /// ```
176    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
177    ///
178    /// let source = "cpp is the best language";
179    ///
180    /// let label = LabeledSpan::at(0..3, "This should be Rust");
181    /// let diag = MietteDiagnostic::new("Wrong best language").with_label(label.clone());
182    /// assert_eq!(diag.message, "Wrong best language");
183    /// assert_eq!(diag.labels, Some(vec![label]));
184    /// ```
185    pub fn with_label(mut self, label: impl Into<LabeledSpan>) -> Self {
186        self.labels = Some(vec![label.into()]);
187        self
188    }
189
190    /// Return new diagnostic with the given labels.
191    ///
192    /// Discards previous labels
193    ///
194    /// # Examples
195    /// ```
196    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
197    ///
198    /// let source = "helo wrld";
199    ///
200    /// let labels = vec![
201    ///     LabeledSpan::at_offset(3, "add 'l'"),
202    ///     LabeledSpan::at_offset(6, "add 'r'"),
203    /// ];
204    /// let diag = MietteDiagnostic::new("Typos in 'hello world'").with_labels(labels.clone());
205    /// assert_eq!(diag.message, "Typos in 'hello world'");
206    /// assert_eq!(diag.labels, Some(labels));
207    /// ```
208    pub fn with_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
209        self.labels = Some(labels.into_iter().collect());
210        self
211    }
212
213    /// Return new diagnostic with new label added to the existing ones.
214    ///
215    /// # Examples
216    /// ```
217    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
218    ///
219    /// let source = "helo wrld";
220    ///
221    /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
222    /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
223    /// let diag = MietteDiagnostic::new("Typos in 'hello world'")
224    ///     .and_label(label1.clone())
225    ///     .and_label(label2.clone());
226    /// assert_eq!(diag.message, "Typos in 'hello world'");
227    /// assert_eq!(diag.labels, Some(vec![label1, label2]));
228    /// ```
229    pub fn and_label(mut self, label: impl Into<LabeledSpan>) -> Self {
230        let mut labels = self.labels.unwrap_or_default();
231        labels.push(label.into());
232        self.labels = Some(labels);
233        self
234    }
235
236    /// Return new diagnostic with new labels added to the existing ones.
237    ///
238    /// # Examples
239    /// ```
240    /// use miette::{Diagnostic, LabeledSpan, MietteDiagnostic};
241    ///
242    /// let source = "helo wrld";
243    ///
244    /// let label1 = LabeledSpan::at_offset(3, "add 'l'");
245    /// let label2 = LabeledSpan::at_offset(6, "add 'r'");
246    /// let label3 = LabeledSpan::at_offset(9, "add '!'");
247    /// let diag = MietteDiagnostic::new("Typos in 'hello world!'")
248    ///     .and_label(label1.clone())
249    ///     .and_labels([label2.clone(), label3.clone()]);
250    /// assert_eq!(diag.message, "Typos in 'hello world!'");
251    /// assert_eq!(diag.labels, Some(vec![label1, label2, label3]));
252    /// ```
253    pub fn and_labels(mut self, labels: impl IntoIterator<Item = LabeledSpan>) -> Self {
254        let mut all_labels = self.labels.unwrap_or_default();
255        all_labels.extend(labels);
256        self.labels = Some(all_labels);
257        self
258    }
259}
260
261#[cfg(feature = "serde")]
262#[test]
263fn test_serialize_miette_diagnostic() {
264    use serde_json::json;
265
266    use crate::diagnostic;
267
268    let diag = diagnostic!("message");
269    let json = json!({ "message": "message" });
270    assert_eq!(json!(diag), json);
271
272    let diag = diagnostic!(
273        code = "code",
274        help = "help",
275        url = "url",
276        labels = [
277            LabeledSpan::at_offset(0, "label1"),
278            LabeledSpan::at(1..3, "label2")
279        ],
280        severity = Severity::Warning,
281        "message"
282    );
283    let json = json!({
284        "message": "message",
285        "code": "code",
286        "help": "help",
287        "url": "url",
288        "severity": "Warning",
289        "labels": [
290            {
291                "span": {
292                    "offset": 0,
293                    "length": 0
294                },
295                "label": "label1",
296                "primary": false
297            },
298            {
299                "span": {
300                    "offset": 1,
301                    "length": 2
302                },
303                "label": "label2",
304                "primary": false
305            }
306        ]
307    });
308    assert_eq!(json!(diag), json);
309}
310
311#[cfg(feature = "serde")]
312#[test]
313fn test_deserialize_miette_diagnostic() {
314    use serde_json::json;
315
316    use crate::diagnostic;
317
318    let json = json!({ "message": "message" });
319    let diag = diagnostic!("message");
320    assert_eq!(diag, serde_json::from_value(json).unwrap());
321
322    let json = json!({
323        "message": "message",
324        "help": null,
325        "code": null,
326        "severity": null,
327        "url": null,
328        "labels": null
329    });
330    assert_eq!(diag, serde_json::from_value(json).unwrap());
331
332    let diag = diagnostic!(
333        code = "code",
334        help = "help",
335        url = "url",
336        labels = [
337            LabeledSpan::at_offset(0, "label1"),
338            LabeledSpan::at(1..3, "label2")
339        ],
340        severity = Severity::Warning,
341        "message"
342    );
343    let json = json!({
344        "message": "message",
345        "code": "code",
346        "help": "help",
347        "url": "url",
348        "severity": "Warning",
349        "labels": [
350            {
351                "span": {
352                    "offset": 0,
353                    "length": 0
354                },
355                "label": "label1",
356                "primary": false
357            },
358            {
359                "span": {
360                    "offset": 1,
361                    "length": 2
362                },
363                "label": "label2",
364                "primary": false
365            }
366        ]
367    });
368    assert_eq!(diag, serde_json::from_value(json).unwrap());
369}