1use crate::Location;
2use std::fmt::{self, Display, Formatter};
3
4#[derive(Debug, Clone, PartialEq)]
12pub enum Error {
13 InvalidUtf8 {
14 location: Box<Location>,
15 },
16 UnsupportedType {
17 text: String,
18 location: Box<Location>,
19 found: &'static str,
20 },
21 Parse {
22 text: String,
23 location: Option<Box<Location>>,
24 message: String,
25 },
26 #[cfg(feature = "serde")]
31 Deserialize {
32 text: String,
33 message: String,
34 location: Option<Box<Location>>,
35 },
36}
37
38impl Error {
39 #[cfg(feature = "serde")]
43 pub(crate) fn or_location(mut self, location: &Location) -> Self {
44 if let Self::Deserialize {
45 location: slot @ None,
46 ..
47 } = &mut self
48 {
49 *slot = Some(Box::new(location.clone()));
50 }
51 self
52 }
53
54 #[cfg(feature = "serde")]
57 pub fn deserialize_location(&self) -> Option<&Location> {
58 match self {
59 Self::Deserialize {
60 location: Some(location),
61 ..
62 } => Some(location),
63 _ => None,
64 }
65 }
66
67 #[cfg(feature = "serde")]
70 pub fn with_source_text(mut self, text: impl Into<String>) -> Self {
71 if let Self::Deserialize { text: slot, .. } = &mut self
72 && slot.is_empty()
73 {
74 *slot = text.into();
75 }
76 self
77 }
78}
79
80fn located_message(location: &Location, message: &str) -> String {
81 format!("{message} at {location}")
82}
83
84impl Display for Error {
85 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
86 match self {
87 Self::InvalidUtf8 { location } => {
88 write!(f, "invalid utf-8 in configuration input from {location}")?;
89 }
90 Self::UnsupportedType {
91 location, found, ..
92 } => {
93 write!(
94 f,
95 "{}",
96 located_message(
97 location,
98 &format!("unsupported configuration input type `{found}`"),
99 )
100 )?;
101 }
102 Self::Parse {
103 location: Some(location),
104 message,
105 ..
106 } => write!(f, "{}", located_message(location, message))?,
107 Self::Parse { message, .. } => write!(f, "{message}")?,
108 #[cfg(feature = "serde")]
109 Self::Deserialize {
110 message,
111 location: Some(location),
112 ..
113 } => write!(
114 f,
115 "{}",
116 located_message(
117 location,
118 &format!("failed to deserialize configuration: {message}"),
119 )
120 )?,
121 #[cfg(feature = "serde")]
122 Self::Deserialize { message, .. } => {
123 write!(f, "failed to deserialize configuration: {message}")?
124 }
125 }
126
127 if !f.alternate() {
128 return Ok(());
129 }
130
131 let (text, location) = match self {
132 Self::UnsupportedType { text, location, .. } => (text.as_str(), location),
133 Self::Parse {
134 text,
135 location: Some(location),
136 ..
137 } => (text.as_str(), location),
138 #[cfg(feature = "serde")]
139 Self::Deserialize {
140 text,
141 location: Some(location),
142 ..
143 } if !text.is_empty() => (text.as_str(), location),
144 _ => return Ok(()),
145 };
146
147 let line_number = location.line.map(|line| line.get() as usize);
148 let column = location.column.map(|column| column.get() as usize);
149 let highlight = location
150 .length
151 .map_or(1, |length| length.get() as usize)
152 .max(1);
153
154 if let Some(line_number) = line_number {
155 let lines: Vec<&str> = text.split('\n').collect();
156 let start = if line_number > 1 { line_number - 2 } else { 0 };
157 let end = if line_number + 1 < lines.len() {
158 line_number + 1
159 } else {
160 lines.len()
161 };
162 let gutter_width = end.to_string().len();
163 let mut line_index = start;
164 while line_index < end {
165 let display_line = line_index + 1;
166 let line_text = display_line.to_string();
167 write!(f, "\n ")?;
168 for _ in 0..gutter_width.saturating_sub(line_text.len()) {
169 write!(f, " ")?;
170 }
171 write!(f, "{line_text} | ")?;
172 write!(f, "{}", lines[line_index])?;
173 if display_line == line_number {
174 write!(f, "\n ")?;
175 for _ in 0..gutter_width.saturating_sub(line_text.len()) {
176 write!(f, " ")?;
177 }
178 for _ in 0..line_text.len() + 1 {
179 write!(f, " ")?;
180 }
181 write!(f, "| ")?;
182 if let Some(column_number) = column {
183 for _ in 1..column_number {
184 write!(f, " ")?;
185 }
186 }
187 for _ in 0..highlight {
188 write!(f, "^")?;
189 }
190 }
191 line_index += 1;
192 }
193 } else {
194 write!(f, "\n {text}")?;
195 if let Some(column_number) = column {
196 write!(f, "\n ")?;
197 for _ in 1..column_number {
198 write!(f, " ")?;
199 }
200 for _ in 0..highlight {
201 write!(f, "^")?;
202 }
203 }
204 }
205
206 Ok(())
207 }
208}
209
210impl std::error::Error for Error {}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::Location;
216
217 #[test]
218 fn default_display_is_single_line() {
219 let error = Error::UnsupportedType {
220 text: "foo: bar\nbaz: datetime\n".to_string(),
221 location: Box::new(Location::at("file", "config.toml", Some(2), Some(7), None)),
222 found: "datetime",
223 };
224 let message = error.to_string();
225 assert!(!message.contains('\n'));
226 assert!(!message.contains('^'));
227 assert!(message.contains("file:config.toml:2:7"));
228 }
229
230 #[test]
231 fn alternate_display_underlines_token() {
232 let error = Error::UnsupportedType {
233 text: "foo: bar\nbaz: datetime\n".to_string(),
234 location: Box::new(Location::at(
235 "file",
236 "config.toml",
237 Some(2),
238 Some(6),
239 Some(8),
240 )),
241 found: "datetime",
242 };
243 let message = format!("{error:#}");
244 assert!(message.contains("^^^^"));
245 assert!(message.contains("baz: datetime"));
246 }
247
248 #[test]
249 fn alternate_display_aligns_gutter_pipe() {
250 let error = Error::UnsupportedType {
251 text: "foo: bar\n\nbaz:\n\n qux: datetime\n".to_string(),
252 location: Box::new(Location::at("file", "config.toml", Some(5), Some(8), None)),
253 found: "datetime",
254 };
255 let message = format!("{error:#}");
256 let source_line = message
257 .lines()
258 .find(|line| line.contains("qux: datetime"))
259 .expect("source line");
260 let underline_line = message
261 .lines()
262 .find(|line| line.contains('^'))
263 .expect("underline line");
264 let source_pipe = source_line.find('|').expect("source pipe");
265 let underline_pipe = underline_line.find('|').expect("underline pipe");
266 assert_eq!(source_pipe, underline_pipe);
267 }
268}