nu_protocol/errors/
labeled_error.rs

1use super::{ShellError, shell_error::io::IoError};
2use crate::{FromValue, IntoValue, Span, Type, Value, record};
3use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
4use serde::{Deserialize, Serialize};
5use std::{fmt, fs};
6
7// # use nu_protocol::{FromValue, Value, ShellError, record, Span};
8
9/// A very generic type of error used for interfacing with external code, such as scripts and
10/// plugins.
11///
12/// This generally covers most of the interface of [`miette::Diagnostic`], but with types that are
13/// well-defined for our protocol.
14#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
15pub struct LabeledError {
16    /// The main message for the error.
17    pub msg: String,
18    /// Labeled spans attached to the error, demonstrating to the user where the problem is.
19    #[serde(default)]
20    pub labels: Box<Vec<ErrorLabel>>,
21    /// A unique machine- and search-friendly error code to associate to the error. (e.g.
22    /// `nu::shell::missing_config_value`)
23    #[serde(default)]
24    pub code: Option<String>,
25    /// A link to documentation about the error, used in conjunction with `code`
26    #[serde(default)]
27    pub url: Option<String>,
28    /// Additional help for the error, usually a hint about what the user might try
29    #[serde(default)]
30    pub help: Option<String>,
31    /// Errors that are related to or caused this error
32    #[serde(default)]
33    pub inner: Box<Vec<ShellError>>,
34}
35
36impl LabeledError {
37    /// Create a new plain [`LabeledError`] with the given message.
38    ///
39    /// This is usually used builder-style with methods like [`.with_label()`](Self::with_label) to
40    /// build an error.
41    ///
42    /// # Example
43    ///
44    /// ```rust
45    /// # use nu_protocol::LabeledError;
46    /// let error = LabeledError::new("Something bad happened");
47    /// assert_eq!("Something bad happened", error.to_string());
48    /// ```
49    pub fn new(msg: impl Into<String>) -> Self {
50        Self {
51            msg: msg.into(),
52            ..Default::default()
53        }
54    }
55
56    /// Add a labeled span to the error to demonstrate to the user where the problem is.
57    ///
58    /// # Example
59    ///
60    /// ```rust
61    /// # use nu_protocol::{LabeledError, Span};
62    /// # let span = Span::test_data();
63    /// let error = LabeledError::new("An error")
64    ///     .with_label("happened here", span);
65    /// assert_eq!("happened here", &error.labels[0].text);
66    /// assert_eq!(span, error.labels[0].span);
67    /// ```
68    pub fn with_label(mut self, text: impl Into<String>, span: Span) -> Self {
69        self.labels.push(ErrorLabel {
70            text: text.into(),
71            span,
72        });
73        self
74    }
75
76    /// Add a unique machine- and search-friendly error code to associate to the error. (e.g.
77    /// `nu::shell::missing_config_value`)
78    ///
79    /// # Example
80    ///
81    /// ```rust
82    /// # use nu_protocol::LabeledError;
83    /// let error = LabeledError::new("An error")
84    ///     .with_code("my_product::error");
85    /// assert_eq!(Some("my_product::error"), error.code.as_deref());
86    /// ```
87    pub fn with_code(mut self, code: impl Into<String>) -> Self {
88        self.code = Some(code.into());
89        self
90    }
91
92    /// Add a link to documentation about the error, used in conjunction with `code`.
93    ///
94    /// # Example
95    ///
96    /// ```rust
97    /// # use nu_protocol::LabeledError;
98    /// let error = LabeledError::new("An error")
99    ///     .with_url("https://example.org/");
100    /// assert_eq!(Some("https://example.org/"), error.url.as_deref());
101    /// ```
102    pub fn with_url(mut self, url: impl Into<String>) -> Self {
103        self.url = Some(url.into());
104        self
105    }
106
107    /// Add additional help for the error, usually a hint about what the user might try.
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// # use nu_protocol::LabeledError;
113    /// let error = LabeledError::new("An error")
114    ///     .with_help("did you try turning it off and back on again?");
115    /// assert_eq!(Some("did you try turning it off and back on again?"), error.help.as_deref());
116    /// ```
117    pub fn with_help(mut self, help: impl Into<String>) -> Self {
118        self.help = Some(help.into());
119        self
120    }
121
122    /// Add an error that is related to or caused this error.
123    ///
124    /// # Example
125    ///
126    /// ```rust
127    /// # use nu_protocol::{LabeledError, ShellError};
128    /// let error = LabeledError::new("An error")
129    ///     .with_inner(LabeledError::new("out of coolant"));
130    /// let check: ShellError = LabeledError::new("out of coolant").into();
131    /// assert_eq!(check, error.inner[0]);
132    /// ```
133    pub fn with_inner(mut self, inner: impl Into<ShellError>) -> Self {
134        let inner_error: ShellError = inner.into();
135        self.inner.push(inner_error);
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)) -> Self {
159        Self {
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(|i| Self::from_diagnostic(i).into())
179                .collect::<Vec<_>>()
180                .into(),
181        }
182    }
183}
184
185/// A labeled span within a [`LabeledError`].
186#[derive(Debug, Default, 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 From<ErrorLabel> for LabeledSpan {
195    fn from(val: ErrorLabel) -> Self {
196        LabeledSpan::new(
197            (!val.text.is_empty()).then_some(val.text),
198            val.span.start,
199            val.span.end - val.span.start,
200        )
201    }
202}
203
204impl From<ErrorLabel> for SourceSpan {
205    fn from(val: ErrorLabel) -> Self {
206        SourceSpan::new(val.span.start.into(), val.span.end - val.span.start)
207    }
208}
209
210impl FromValue for ErrorLabel {
211    fn from_value(v: Value) -> Result<Self, ShellError> {
212        let record = v.clone().into_record()?;
213        let text = String::from_value(match record.get("text") {
214            Some(val) => val.clone(),
215            None => Value::string("", v.span()),
216        })
217        .unwrap_or("originates from here".into());
218        let span = Span::from_value(match record.get("span") {
219            Some(val) => val.clone(),
220            // Maybe there's a better way...
221            None => Value::record(
222                record! {
223                    "start" => Value::int(v.span().start as i64, v.span()),
224                    "end" => Value::int(v.span().end as i64, v.span()),
225                },
226                v.span(),
227            ),
228        });
229
230        match span {
231            Ok(s) => Ok(Self { text, span: s }),
232            Err(e) => Err(e),
233        }
234    }
235    fn expected_type() -> crate::Type {
236        Type::Record(
237            vec![
238                ("text".into(), Type::String),
239                ("span".into(), Type::record()),
240            ]
241            .into(),
242        )
243    }
244}
245
246impl IntoValue for ErrorLabel {
247    fn into_value(self, span: Span) -> Value {
248        record! {
249            "text" => Value::string(self.text, span),
250            "span" => span.into_value(span),
251        }
252        .into_value(span)
253    }
254}
255
256/// Optionally named error source
257#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct ErrorSource {
259    name: Option<String>,
260    text: Option<String>,
261    path: Option<String>,
262}
263
264impl ErrorSource {
265    pub fn new(name: Option<String>, text: String) -> Self {
266        Self {
267            name,
268            text: Some(text),
269            path: None,
270        }
271    }
272}
273
274impl From<ErrorSource> for NamedSource<String> {
275    fn from(value: ErrorSource) -> Self {
276        let name = value.name.unwrap_or_default();
277        match value {
278            ErrorSource {
279                text: Some(text),
280                path: None,
281                ..
282            } => NamedSource::new(name, text),
283            ErrorSource {
284                text: None,
285                path: Some(path),
286                ..
287            } => {
288                let text = fs::read_to_string(&path).unwrap_or_default();
289                NamedSource::new(path, text)
290            }
291            _ => NamedSource::new(name, "".into()),
292        }
293    }
294}
295
296impl FromValue for ErrorSource {
297    fn from_value(v: Value) -> Result<Self, ShellError> {
298        let record = v.clone().into_record()?;
299        let name = record
300            .get("name")
301            .and_then(|s| String::from_value(s.clone()).ok());
302        // let name = String::from_value(record.get("name").unwrap().clone()).ok();
303
304        let text = if let Some(text) = record.get("text") {
305            String::from_value(text.clone()).ok()
306        } else {
307            None
308        };
309        let path = if let Some(path) = record.get("path") {
310            String::from_value(path.clone()).ok()
311        } else {
312            None
313        };
314
315        match (text, path) {
316            // Prioritize not reading from a file and using the text raw
317            (text @ Some(_), _) => Ok(ErrorSource {
318                name,
319                text,
320                path: None,
321            }),
322            (_, path @ Some(_)) => Ok(ErrorSource {
323                name: path.clone(),
324                text: None,
325                path,
326            }),
327            _ => Err(ShellError::CantConvert {
328                to_type: Self::expected_type().to_string(),
329                from_type: v.get_type().to_string(),
330                span: v.span(),
331                help: None,
332            }),
333        }
334    }
335    fn expected_type() -> crate::Type {
336        Type::Record(
337            vec![
338                ("name".into(), Type::String),
339                ("text".into(), Type::String),
340                ("path".into(), Type::String),
341            ]
342            .into(),
343        )
344    }
345}
346
347impl IntoValue for ErrorSource {
348    fn into_value(self, span: Span) -> Value {
349        match self {
350            Self {
351                name: Some(name),
352                text: Some(text),
353                ..
354            } => record! {
355                "name" => Value::string(name, span),
356                "text" => Value::string(text, span),
357            },
358            Self {
359                text: Some(text), ..
360            } => record! {
361                "text" => Value::string(text, span)
362            },
363            Self {
364                name: Some(name),
365                path: Some(path),
366                ..
367            } => record! {
368                "name" => Value::string(name, span),
369                "path" => Value::string(path, span),
370            },
371            Self {
372                path: Some(path), ..
373            } => record! {
374                "path" => Value::string(path, span),
375            },
376            _ => record! {},
377        }
378        .into_value(span)
379    }
380}
381
382impl fmt::Display for LabeledError {
383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
384        f.write_str(&self.msg)
385    }
386}
387
388impl std::error::Error for LabeledError {
389    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
390        self.inner.first().map(|r| r as _)
391    }
392}
393
394impl Diagnostic for LabeledError {
395    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
396        self.code.as_ref().map(Box::new).map(|b| b as _)
397    }
398
399    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
400        self.help.as_ref().map(Box::new).map(|b| b as _)
401    }
402
403    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
404        self.url.as_ref().map(Box::new).map(|b| b as _)
405    }
406
407    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
408        Some(Box::new(
409            self.labels.iter().map(|label| label.clone().into()),
410        ))
411    }
412
413    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
414        Some(Box::new(self.inner.iter().map(|r| r as _)))
415    }
416}
417
418impl From<ShellError> for LabeledError {
419    fn from(err: ShellError) -> Self {
420        Self::from_diagnostic(&err)
421    }
422}
423
424impl From<IoError> for LabeledError {
425    fn from(err: IoError) -> Self {
426        Self::from_diagnostic(&err)
427    }
428}