nu_protocol/errors/
labeled_error.rs

1use super::{ShellError, shell_error::io::IoError};
2use crate::Span;
3use miette::Diagnostic;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// A very generic type of error used for interfacing with external code, such as scripts and
8/// plugins.
9///
10/// This generally covers most of the interface of [`miette::Diagnostic`], but with types that are
11/// well-defined for our protocol.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct LabeledError {
14    /// The main message for the error.
15    pub msg: String,
16    /// Labeled spans attached to the error, demonstrating to the user where the problem is.
17    #[serde(default)]
18    pub labels: Box<Vec<ErrorLabel>>,
19    /// A unique machine- and search-friendly error code to associate to the error. (e.g.
20    /// `nu::shell::missing_config_value`)
21    #[serde(default)]
22    pub code: Option<String>,
23    /// A link to documentation about the error, used in conjunction with `code`
24    #[serde(default)]
25    pub url: Option<String>,
26    /// Additional help for the error, usually a hint about what the user might try
27    #[serde(default)]
28    pub help: Option<String>,
29    /// Errors that are related to or caused this error
30    #[serde(default)]
31    pub inner: Box<Vec<LabeledError>>,
32}
33
34impl LabeledError {
35    /// Create a new plain [`LabeledError`] with the given message.
36    ///
37    /// This is usually used builder-style with methods like [`.with_label()`](Self::with_label) to
38    /// build an error.
39    ///
40    /// # Example
41    ///
42    /// ```rust
43    /// # use nu_protocol::LabeledError;
44    /// let error = LabeledError::new("Something bad happened");
45    /// assert_eq!("Something bad happened", error.to_string());
46    /// ```
47    pub fn new(msg: impl Into<String>) -> LabeledError {
48        LabeledError {
49            msg: msg.into(),
50            labels: Box::new(vec![]),
51            code: None,
52            url: None,
53            help: None,
54            inner: Box::new(vec![]),
55        }
56    }
57
58    /// Add a labeled span to the error to demonstrate to the user where the problem is.
59    ///
60    /// # Example
61    ///
62    /// ```rust
63    /// # use nu_protocol::{LabeledError, Span};
64    /// # let span = Span::test_data();
65    /// let error = LabeledError::new("An error")
66    ///     .with_label("happened here", span);
67    /// assert_eq!("happened here", &error.labels[0].text);
68    /// assert_eq!(span, error.labels[0].span);
69    /// ```
70    pub fn with_label(mut self, text: impl Into<String>, span: Span) -> Self {
71        self.labels.push(ErrorLabel {
72            text: text.into(),
73            span,
74        });
75        self
76    }
77
78    /// Add a unique machine- and search-friendly error code to associate to the error. (e.g.
79    /// `nu::shell::missing_config_value`)
80    ///
81    /// # Example
82    ///
83    /// ```rust
84    /// # use nu_protocol::LabeledError;
85    /// let error = LabeledError::new("An error")
86    ///     .with_code("my_product::error");
87    /// assert_eq!(Some("my_product::error"), error.code.as_deref());
88    /// ```
89    pub fn with_code(mut self, code: impl Into<String>) -> Self {
90        self.code = Some(code.into());
91        self
92    }
93
94    /// Add a link to documentation about the error, used in conjunction with `code`.
95    ///
96    /// # Example
97    ///
98    /// ```rust
99    /// # use nu_protocol::LabeledError;
100    /// let error = LabeledError::new("An error")
101    ///     .with_url("https://example.org/");
102    /// assert_eq!(Some("https://example.org/"), error.url.as_deref());
103    /// ```
104    pub fn with_url(mut self, url: impl Into<String>) -> Self {
105        self.url = Some(url.into());
106        self
107    }
108
109    /// Add additional help for the error, usually a hint about what the user might try.
110    ///
111    /// # Example
112    ///
113    /// ```rust
114    /// # use nu_protocol::LabeledError;
115    /// let error = LabeledError::new("An error")
116    ///     .with_help("did you try turning it off and back on again?");
117    /// assert_eq!(Some("did you try turning it off and back on again?"), error.help.as_deref());
118    /// ```
119    pub fn with_help(mut self, help: impl Into<String>) -> Self {
120        self.help = Some(help.into());
121        self
122    }
123
124    /// Add an error that is related to or caused this error.
125    ///
126    /// # Example
127    ///
128    /// ```rust
129    /// # use nu_protocol::LabeledError;
130    /// let error = LabeledError::new("An error")
131    ///     .with_inner(LabeledError::new("out of coolant"));
132    /// assert_eq!(LabeledError::new("out of coolant"), error.inner[0]);
133    /// ```
134    pub fn with_inner(mut self, inner: impl Into<LabeledError>) -> Self {
135        self.inner.push(inner.into());
136        self
137    }
138
139    /// Create a [`LabeledError`] from a type that implements [`miette::Diagnostic`].
140    ///
141    /// # Example
142    ///
143    /// [`ShellError`] implements `miette::Diagnostic`:
144    ///
145    /// ```rust
146    /// # use nu_protocol::{ShellError, LabeledError, shell_error::{self, io::IoError}, Span};
147    /// #
148    /// let error = LabeledError::from_diagnostic(
149    ///     &ShellError::Io(IoError::new_with_additional_context(
150    ///         shell_error::io::ErrorKind::from_std(std::io::ErrorKind::Other),
151    ///         Span::test_data(),
152    ///         None,
153    ///         "some error"
154    ///     ))
155    /// );
156    /// assert!(error.to_string().contains("I/O error"));
157    /// ```
158    pub fn from_diagnostic(diag: &(impl miette::Diagnostic + ?Sized)) -> LabeledError {
159        LabeledError {
160            msg: diag.to_string(),
161            labels: diag
162                .labels()
163                .into_iter()
164                .flatten()
165                .map(|label| ErrorLabel {
166                    text: label.label().unwrap_or("").into(),
167                    span: Span::new(label.offset(), label.offset() + label.len()),
168                })
169                .collect::<Vec<_>>()
170                .into(),
171            code: diag.code().map(|s| s.to_string()),
172            url: diag.url().map(|s| s.to_string()),
173            help: diag.help().map(|s| s.to_string()),
174            inner: diag
175                .related()
176                .into_iter()
177                .flatten()
178                .map(Self::from_diagnostic)
179                .collect::<Vec<_>>()
180                .into(),
181        }
182    }
183}
184
185/// A labeled span within a [`LabeledError`].
186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
187pub struct ErrorLabel {
188    /// Text to show together with the span
189    pub text: String,
190    /// Span pointing at where the text references in the source
191    pub span: Span,
192}
193
194impl fmt::Display for LabeledError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.write_str(&self.msg)
197    }
198}
199
200impl std::error::Error for LabeledError {
201    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202        self.inner.first().map(|r| r as _)
203    }
204}
205
206impl Diagnostic for LabeledError {
207    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
208        self.code.as_ref().map(Box::new).map(|b| b as _)
209    }
210
211    fn severity(&self) -> Option<miette::Severity> {
212        None
213    }
214
215    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
216        self.help.as_ref().map(Box::new).map(|b| b as _)
217    }
218
219    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
220        self.url.as_ref().map(Box::new).map(|b| b as _)
221    }
222
223    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
224        None
225    }
226
227    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
228        Some(Box::new(self.labels.iter().map(|label| {
229            miette::LabeledSpan::new_with_span(
230                Some(label.text.clone()).filter(|s| !s.is_empty()),
231                label.span,
232            )
233        })))
234    }
235
236    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
237        Some(Box::new(self.inner.iter().map(|r| r as _)))
238    }
239
240    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
241        None
242    }
243}
244
245impl From<ShellError> for LabeledError {
246    fn from(err: ShellError) -> Self {
247        LabeledError::from_diagnostic(&err)
248    }
249}
250
251impl From<IoError> for LabeledError {
252    fn from(err: IoError) -> Self {
253        LabeledError::from_diagnostic(&err)
254    }
255}