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 UnsupportedNull {
17 text: String,
18 location: Box<Location>,
19 },
20 UnsupportedType {
21 text: String,
22 location: Box<Location>,
23 found: &'static str,
24 },
25 Parse {
26 text: String,
27 location: Option<Box<Location>>,
28 message: String,
29 },
30}
31
32fn located_message(location: &Location, message: &str) -> String {
33 format!("{message} at {location}")
34}
35
36impl Display for Error {
37 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::InvalidUtf8 { location } => {
40 write!(f, "invalid utf-8 in configuration input from {location}")?;
41 }
42 Self::UnsupportedNull { location, .. } => {
43 write!(
44 f,
45 "{}",
46 located_message(
47 location,
48 "null values are not supported in configuration input",
49 )
50 )?;
51 }
52 Self::UnsupportedType {
53 location, found, ..
54 } => {
55 write!(
56 f,
57 "{}",
58 located_message(
59 location,
60 &format!("unsupported configuration input type `{found}`"),
61 )
62 )?;
63 }
64 Self::Parse {
65 location: Some(location),
66 message,
67 ..
68 } => write!(f, "{}", located_message(location, message))?,
69 Self::Parse { message, .. } => write!(f, "{message}")?,
70 }
71
72 if !f.alternate() {
73 return Ok(());
74 }
75
76 let (text, location) = match self {
77 Self::UnsupportedNull { text, location, .. }
78 | Self::UnsupportedType { text, location, .. } => (text.as_str(), location),
79 Self::Parse {
80 text,
81 location: Some(location),
82 ..
83 } => (text.as_str(), location),
84 _ => return Ok(()),
85 };
86
87 let line_number = location.line.map(|line| line.get() as usize);
88 let column = location.column.map(|column| column.get() as usize);
89 let highlight = location
90 .length
91 .map_or(1, |length| length.get() as usize)
92 .max(1);
93
94 if let Some(line_number) = line_number {
95 let lines: Vec<&str> = text.split('\n').collect();
96 let start = if line_number > 1 { line_number - 2 } else { 0 };
97 let end = if line_number + 1 < lines.len() {
98 line_number + 1
99 } else {
100 lines.len()
101 };
102 let gutter_width = end.to_string().len();
103 let mut line_index = start;
104 while line_index < end {
105 let display_line = line_index + 1;
106 let line_text = display_line.to_string();
107 write!(f, "\n ")?;
108 for _ in 0..gutter_width.saturating_sub(line_text.len()) {
109 write!(f, " ")?;
110 }
111 write!(f, "{line_text} | ")?;
112 write!(f, "{}", lines[line_index])?;
113 if display_line == line_number {
114 write!(f, "\n ")?;
115 for _ in 0..gutter_width.saturating_sub(line_text.len()) {
116 write!(f, " ")?;
117 }
118 for _ in 0..line_text.len() + 1 {
119 write!(f, " ")?;
120 }
121 write!(f, "| ")?;
122 if let Some(column_number) = column {
123 for _ in 1..column_number {
124 write!(f, " ")?;
125 }
126 }
127 for _ in 0..highlight {
128 write!(f, "^")?;
129 }
130 }
131 line_index += 1;
132 }
133 } else {
134 write!(f, "\n {text}")?;
135 if let Some(column_number) = column {
136 write!(f, "\n ")?;
137 for _ in 1..column_number {
138 write!(f, " ")?;
139 }
140 for _ in 0..highlight {
141 write!(f, "^")?;
142 }
143 }
144 }
145
146 Ok(())
147 }
148}
149
150impl std::error::Error for Error {}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::Location;
156
157 #[test]
158 fn default_display_is_single_line() {
159 let error = Error::UnsupportedNull {
160 text: "foo: bar\nbaz: ~\n".to_string(),
161 location: Box::new(Location::at("file", "config.yaml", Some(2), Some(7), None)),
162 };
163 let message = error.to_string();
164 assert!(!message.contains('\n'));
165 assert!(!message.contains('^'));
166 assert!(message.contains("file:config.yaml:2:7"));
167 }
168
169 #[test]
170 fn alternate_display_underlines_token() {
171 let error = Error::UnsupportedNull {
172 text: "foo: bar\nbaz: null\n".to_string(),
173 location: Box::new(Location::at(
174 "file",
175 "config.yaml",
176 Some(2),
177 Some(6),
178 Some(4),
179 )),
180 };
181 let message = format!("{error:#}");
182 assert!(message.contains("^^^^"));
183 assert!(message.contains("baz: null"));
184 }
185
186 #[test]
187 fn alternate_display_aligns_gutter_pipe() {
188 let error = Error::UnsupportedNull {
189 text: "foo: bar\n\nbaz:\n\n qux: ~\n".to_string(),
190 location: Box::new(Location::at("file", "config.yaml", Some(5), Some(8), None)),
191 };
192 let message = format!("{error:#}");
193 let source_line = message
194 .lines()
195 .find(|line| line.contains("qux: ~"))
196 .expect("source line");
197 let underline_line = message
198 .lines()
199 .find(|line| line.contains('^'))
200 .expect("underline line");
201 let source_pipe = source_line.find('|').expect("source pipe");
202 let underline_pipe = underline_line.find('|').expect("underline pipe");
203 assert_eq!(source_pipe, underline_pipe);
204 }
205}