1use std::fmt;
32use std::path::PathBuf;
33
34use thiserror::Error;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
43#[non_exhaustive]
44pub struct FileParseError {
45 pub format: &'static str,
47 pub message: Box<str>,
49 pub line: Option<usize>,
51 pub column: Option<usize>,
53}
54
55impl fmt::Display for FileParseError {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(&self.message)
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
68#[non_exhaustive]
69pub struct ParseDiagnostic {
70 pub message: Box<str>,
72 pub line: usize,
74 pub column: usize,
76}
77
78impl fmt::Display for ParseDiagnostic {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.write_str(&self.message)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Error)]
86#[error("parse error in {display_name}: {diagnostic}")]
87#[non_exhaustive]
88pub struct ParseError {
89 pub display_name: String,
91 pub source_text: Box<str>,
93 pub start_line: usize,
95 pub diagnostic: ParseDiagnostic,
97}
98
99impl ParseError {
100 fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
101 self.display_name = display_name.into();
102 self
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Error)]
108#[error("config error in {path}: {details}")]
109#[non_exhaustive]
110pub struct ConfigError {
111 pub path: PathBuf,
113 pub details: FileParseError,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Error)]
119#[error("spec error in {path}: {details}")]
120#[non_exhaustive]
121pub struct SpecError {
122 pub path: PathBuf,
124 pub details: FileParseError,
126}
127
128#[derive(Debug, Error)]
131#[non_exhaustive]
132pub enum Error {
133 #[error("{0}")]
135 Parse(#[from] ParseError),
136
137 #[error("{0}")]
139 Config(#[from] ConfigError),
140
141 #[error("{0}")]
143 Spec(#[from] SpecError),
144
145 #[error("I/O error: {0}")]
147 Io(#[from] std::io::Error),
148
149 #[error("formatter error: {0}")]
155 Formatter(String),
156
157 #[error(
160 "line {line_no} is {width} characters wide, exceeding the configured limit of {limit}"
161 )]
162 LayoutTooWide {
163 line_no: usize,
165 width: usize,
167 limit: usize,
169 },
170}
171
172pub type Result<T> = std::result::Result<T, Error>;
174
175impl Error {
176 pub fn with_display_name(self, display_name: impl Into<String>) -> Self {
182 match self {
183 Self::Parse(parse) => Self::Parse(parse.with_display_name(display_name)),
184 other => other,
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn parse_diagnostic_display_shows_message() {
195 let diag = ParseDiagnostic {
196 message: "expected argument part".into(),
197 line: 5,
198 column: 10,
199 };
200 assert_eq!(diag.to_string(), "expected argument part");
201 }
202
203 #[test]
204 fn parse_diagnostic_from_parse_error() {
205 let source = "if(\n";
206 let err = crate::parser::parse(source).unwrap_err();
207 if let Error::Parse(ParseError { diagnostic, .. }) = err {
208 assert!(diagnostic.line >= 1);
209 assert!(diagnostic.column >= 1);
210 assert!(!diagnostic.message.is_empty());
211 } else {
212 panic!("expected Parse, got {err:?}");
213 }
214 }
215
216 #[test]
217 fn error_parse_display() {
218 let err = Error::Parse(ParseError {
219 display_name: "test.cmake".to_owned(),
220 source_text: "if(\n".into(),
221 start_line: 1,
222 diagnostic: ParseDiagnostic {
223 message: "expected argument part".into(),
224 line: 1,
225 column: 4,
226 },
227 });
228 let msg = err.to_string();
229 assert!(msg.contains("test.cmake"));
230 assert!(msg.contains("expected argument part"));
231 }
232
233 #[test]
234 fn error_config_display() {
235 let err = Error::Config(ConfigError {
236 path: std::path::PathBuf::from("bad.yaml"),
237 details: FileParseError {
238 format: "YAML",
239 message: "unexpected key".into(),
240 line: Some(3),
241 column: Some(1),
242 },
243 });
244 let msg = err.to_string();
245 assert!(msg.contains("bad.yaml"));
246 assert!(msg.contains("unexpected key"));
247 }
248
249 #[test]
250 fn error_spec_display() {
251 let err = Error::Spec(SpecError {
252 path: std::path::PathBuf::from("commands.yaml"),
253 details: FileParseError {
254 format: "YAML",
255 message: "invalid nargs".into(),
256 line: None,
257 column: None,
258 },
259 });
260 let msg = err.to_string();
261 assert!(msg.contains("commands.yaml"));
262 assert!(msg.contains("invalid nargs"));
263 }
264
265 #[test]
266 fn error_io_display() {
267 let err = Error::Io(std::io::Error::new(
268 std::io::ErrorKind::NotFound,
269 "file not found",
270 ));
271 assert!(err.to_string().contains("file not found"));
272 }
273
274 #[test]
275 fn error_formatter_display() {
276 let err = Error::Formatter("something went wrong".to_owned());
277 assert!(err.to_string().contains("something went wrong"));
278 }
279
280 #[test]
281 fn error_layout_too_wide_display() {
282 let err = Error::LayoutTooWide {
283 line_no: 42,
284 width: 120,
285 limit: 80,
286 };
287 let msg = err.to_string();
288 assert!(msg.contains("42"));
289 assert!(msg.contains("120"));
290 assert!(msg.contains("80"));
291 }
292
293 #[test]
294 fn with_display_name_updates_parse() {
295 let err = Error::Parse(ParseError {
296 display_name: "original".to_owned(),
297 source_text: "set(\n".into(),
298 start_line: 1,
299 diagnostic: ParseDiagnostic {
300 message: "test".into(),
301 line: 1,
302 column: 5,
303 },
304 });
305 let renamed = err.with_display_name("renamed.cmake");
306 match renamed {
307 Error::Parse(ParseError { display_name, .. }) => {
308 assert_eq!(display_name, "renamed.cmake");
309 }
310 _ => panic!("expected Parse"),
311 }
312 }
313
314 #[test]
315 fn with_display_name_passes_through_non_parse_errors() {
316 let err = Error::Formatter("test".to_owned());
317 let result = err.with_display_name("ignored");
318 match result {
319 Error::Formatter(msg) => assert_eq!(msg, "test"),
320 _ => panic!("expected Formatter to pass through"),
321 }
322 }
323
324 #[test]
325 fn io_error_converts_from_std() {
326 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
327 let err: Error = io_err.into();
328 match err {
329 Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied),
330 _ => panic!("expected Io variant"),
331 }
332 }
333}