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}