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 location: Box<Location>,
18 found: &'static str,
19 },
20 Parse {
21 location: Option<Box<Location>>,
22 message: String,
23 },
24 #[cfg(feature = "serde")]
31 Deserialize {
32 message: String,
33 location: Option<Box<Location>>,
34 },
35}
36
37impl Error {
38 #[cfg(feature = "serde")]
42 pub(crate) fn or_location(mut self, location: &Location) -> Self {
43 if let Self::Deserialize {
44 location: slot @ None,
45 ..
46 } = &mut self
47 {
48 *slot = Some(Box::new(location.clone()));
49 }
50 self
51 }
52}
53
54fn located_message(location: &Location, message: &str) -> String {
55 format!("{message} at {location}")
56}
57
58impl Display for Error {
59 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::InvalidUtf8 { location } => {
62 write!(f, "invalid utf-8 in configuration input from {location}")?;
63 }
64 Self::UnsupportedType { location, found } => {
65 write!(
66 f,
67 "{}",
68 located_message(
69 location,
70 &format!("unsupported configuration input type `{found}`"),
71 )
72 )?;
73 }
74 Self::Parse {
75 location: Some(location),
76 message,
77 } => write!(f, "{}", located_message(location, message))?,
78 Self::Parse { message, .. } => write!(f, "{message}")?,
79 #[cfg(feature = "serde")]
80 Self::Deserialize {
81 message,
82 location: Some(location),
83 } => write!(
84 f,
85 "{}",
86 located_message(
87 location,
88 &format!("failed to deserialize configuration: {message}"),
89 )
90 )?,
91 #[cfg(feature = "serde")]
92 Self::Deserialize { message, .. } => {
93 write!(f, "failed to deserialize configuration: {message}")?
94 }
95 }
96
97 if f.alternate() {
100 let location = match self {
101 Self::InvalidUtf8 { location } | Self::UnsupportedType { location, .. } => {
102 Some(location.as_ref())
103 }
104 Self::Parse {
105 location: Some(location),
106 ..
107 } => Some(location.as_ref()),
108 #[cfg(feature = "serde")]
109 Self::Deserialize {
110 location: Some(location),
111 ..
112 } => Some(location.as_ref()),
113 _ => None,
114 };
115 if let Some(location) = location
116 && !location.snippet.is_empty()
117 {
118 write!(f, "\n{}", location.snippet)?;
119 }
120 }
121
122 Ok(())
123 }
124}
125
126impl std::error::Error for Error {}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::Location;
132 use tanzim_source::Source;
133
134 fn source() -> Source {
135 Source::named("file").with_resource("config.toml")
136 }
137
138 #[test]
139 fn default_display_is_single_line() {
140 let error = Error::UnsupportedType {
141 location: Box::new(Location::at("file", "config.toml", Some(2), Some(7), None)),
142 found: "datetime",
143 };
144 let message = error.to_string();
145 assert!(!message.contains('\n'));
146 assert!(!message.contains('^'));
147 assert!(message.contains("file:config.toml:2:7"));
148 }
149
150 #[test]
151 fn alternate_display_underlines_token() {
152 let text = "foo: bar\nbaz: datetime\n";
153 let error = Error::UnsupportedType {
154 location: Box::new(Location::in_text(source(), text, Some(2), Some(6), Some(8))),
155 found: "datetime",
156 };
157 let message = format!("{error:#}");
158 assert!(message.contains("^^^^"));
159 assert!(message.contains("baz: datetime"));
160 }
161
162 #[test]
163 fn alternate_display_aligns_gutter_pipe() {
164 let text = "foo: bar\n\nbaz:\n\n qux: datetime\n";
165 let error = Error::UnsupportedType {
166 location: Box::new(Location::in_text(source(), text, Some(5), Some(8), None)),
167 found: "datetime",
168 };
169 let message = format!("{error:#}");
170 let source_line = message
171 .lines()
172 .find(|line| line.contains("qux: datetime"))
173 .expect("source line");
174 let underline_line = message
175 .lines()
176 .find(|line| line.contains('^'))
177 .expect("underline line");
178 let source_pipe = source_line.find('|').expect("source pipe");
179 let underline_pipe = underline_line.find('|').expect("underline pipe");
180 assert_eq!(source_pipe, underline_pipe);
181 }
182
183 #[cfg(feature = "serde")]
184 #[test]
185 fn deserialize_error_renders_caret_without_attach_step() {
186 let text = "name: app\nport: nope\n";
189 let location = Location::in_text(source(), text, Some(2), Some(7), Some(4));
190 let error = Error::Deserialize {
191 message: "invalid type: string, expected u16".to_string(),
192 location: None,
193 }
194 .or_location(&location);
195 let plain = error.to_string();
196 assert!(!plain.contains('\n'));
197 assert!(!plain.contains('^'));
198 let alternate = format!("{error:#}");
199 assert!(alternate.contains("port: nope"));
200 assert!(alternate.contains("^^^^"));
201 }
202}