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