1use std::fmt;
8use std::path::PathBuf;
9
10use thiserror::Error;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct FileParseError {
17 pub format: &'static str,
19 pub message: Box<str>,
21 pub line: Option<usize>,
23 pub column: Option<usize>,
25}
26
27impl fmt::Display for FileParseError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.write_str(&self.message)
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct ParseDiagnostic {
38 pub message: Box<str>,
40 pub line: usize,
42 pub column: usize,
44}
45
46impl fmt::Display for ParseDiagnostic {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(&self.message)
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Error)]
54#[error("parse error in {display_name}: {diagnostic}")]
55#[non_exhaustive]
56pub struct ParseError {
57 pub display_name: String,
59 pub source_text: Box<str>,
61 pub start_line: usize,
63 pub diagnostic: ParseDiagnostic,
65}
66
67impl ParseError {
68 fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
69 self.display_name = display_name.into();
70 self
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Error)]
76#[error("config error in {path}: {details}")]
77#[non_exhaustive]
78pub struct ConfigError {
79 pub path: PathBuf,
81 pub details: FileParseError,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Error)]
87#[error("spec error in {path}: {details}")]
88#[non_exhaustive]
89pub struct SpecError {
90 pub path: PathBuf,
92 pub details: FileParseError,
94}
95
96#[derive(Debug, Error)]
99#[non_exhaustive]
100pub enum Error {
101 #[error("{0}")]
103 Parse(#[from] ParseError),
104
105 #[error("{0}")]
107 Config(#[from] ConfigError),
108
109 #[error("{0}")]
111 Spec(#[from] SpecError),
112
113 #[error("I/O error: {0}")]
115 Io(#[from] std::io::Error),
116
117 #[error("formatter error: {0}")]
120 Formatter(String),
121
122 #[error(
125 "line {line_no} is {width} characters wide, exceeding the configured limit of {limit}"
126 )]
127 LayoutTooWide {
128 line_no: usize,
130 width: usize,
132 limit: usize,
134 },
135}
136
137pub type Result<T> = std::result::Result<T, Error>;
139
140impl Error {
141 pub fn with_display_name(self, display_name: impl Into<String>) -> Self {
143 match self {
144 Self::Parse(parse) => Self::Parse(parse.with_display_name(display_name)),
145 other => other,
146 }
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn parse_diagnostic_display_shows_message() {
156 let diag = ParseDiagnostic {
157 message: "expected argument part".into(),
158 line: 5,
159 column: 10,
160 };
161 assert_eq!(diag.to_string(), "expected argument part");
162 }
163
164 #[test]
165 fn parse_diagnostic_from_parse_error() {
166 let source = "if(\n";
167 let err = crate::parser::parse(source).unwrap_err();
168 if let Error::Parse(ParseError { diagnostic, .. }) = err {
169 assert!(diagnostic.line >= 1);
170 assert!(diagnostic.column >= 1);
171 assert!(!diagnostic.message.is_empty());
172 } else {
173 panic!("expected Parse, got {err:?}");
174 }
175 }
176
177 #[test]
178 fn error_parse_display() {
179 let err = Error::Parse(ParseError {
180 display_name: "test.cmake".to_owned(),
181 source_text: "if(\n".into(),
182 start_line: 1,
183 diagnostic: ParseDiagnostic {
184 message: "expected argument part".into(),
185 line: 1,
186 column: 4,
187 },
188 });
189 let msg = err.to_string();
190 assert!(msg.contains("test.cmake"));
191 assert!(msg.contains("expected argument part"));
192 }
193
194 #[test]
195 fn error_config_display() {
196 let err = Error::Config(ConfigError {
197 path: std::path::PathBuf::from("bad.yaml"),
198 details: FileParseError {
199 format: "YAML",
200 message: "unexpected key".into(),
201 line: Some(3),
202 column: Some(1),
203 },
204 });
205 let msg = err.to_string();
206 assert!(msg.contains("bad.yaml"));
207 assert!(msg.contains("unexpected key"));
208 }
209
210 #[test]
211 fn error_spec_display() {
212 let err = Error::Spec(SpecError {
213 path: std::path::PathBuf::from("commands.yaml"),
214 details: FileParseError {
215 format: "YAML",
216 message: "invalid nargs".into(),
217 line: None,
218 column: None,
219 },
220 });
221 let msg = err.to_string();
222 assert!(msg.contains("commands.yaml"));
223 assert!(msg.contains("invalid nargs"));
224 }
225
226 #[test]
227 fn error_io_display() {
228 let err = Error::Io(std::io::Error::new(
229 std::io::ErrorKind::NotFound,
230 "file not found",
231 ));
232 assert!(err.to_string().contains("file not found"));
233 }
234
235 #[test]
236 fn error_formatter_display() {
237 let err = Error::Formatter("something went wrong".to_owned());
238 assert!(err.to_string().contains("something went wrong"));
239 }
240
241 #[test]
242 fn error_layout_too_wide_display() {
243 let err = Error::LayoutTooWide {
244 line_no: 42,
245 width: 120,
246 limit: 80,
247 };
248 let msg = err.to_string();
249 assert!(msg.contains("42"));
250 assert!(msg.contains("120"));
251 assert!(msg.contains("80"));
252 }
253
254 #[test]
255 fn with_display_name_updates_parse() {
256 let err = Error::Parse(ParseError {
257 display_name: "original".to_owned(),
258 source_text: "set(\n".into(),
259 start_line: 1,
260 diagnostic: ParseDiagnostic {
261 message: "test".into(),
262 line: 1,
263 column: 5,
264 },
265 });
266 let renamed = err.with_display_name("renamed.cmake");
267 match renamed {
268 Error::Parse(ParseError { display_name, .. }) => {
269 assert_eq!(display_name, "renamed.cmake");
270 }
271 _ => panic!("expected Parse"),
272 }
273 }
274
275 #[test]
276 fn with_display_name_passes_through_non_parse_errors() {
277 let err = Error::Formatter("test".to_owned());
278 let result = err.with_display_name("ignored");
279 match result {
280 Error::Formatter(msg) => assert_eq!(msg, "test"),
281 _ => panic!("expected Formatter to pass through"),
282 }
283 }
284
285 #[test]
286 fn io_error_converts_from_std() {
287 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
288 let err: Error = io_err.into();
289 match err {
290 Error::Io(e) => assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied),
291 _ => panic!("expected Io variant"),
292 }
293 }
294}