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