1use std::fmt::Write;
10
11pub type Result<T> = std::result::Result<T, Error>;
13
14#[derive(Debug, thiserror::Error)]
16pub enum Error {
17 #[error("Parse error at line {line}, column {col}: {message}")]
19 Parse {
20 message: String,
22 line: usize,
24 col: usize,
26 snippet: Option<String>,
28 help: Option<String>,
30 },
31
32 #[error("Unexpected end of input")]
34 UnexpectedEof {
35 expected: String,
37 line: usize,
39 },
40
41 #[error("Invalid directive: {name}")]
43 InvalidDirective {
44 name: String,
46 reason: Option<String>,
48 suggestion: Option<String>,
50 },
51
52 #[error("Invalid argument for directive '{directive}': {message}")]
54 InvalidArgument {
55 directive: String,
57 message: String,
59 expected: Option<String>,
61 },
62
63 #[error("Syntax error: {message}")]
65 Syntax {
66 message: String,
68 line: usize,
70 col: usize,
72 expected: Option<String>,
74 found: Option<String>,
76 },
77
78 #[error("IO error: {0}")]
80 Io(#[from] std::io::Error),
81
82 #[cfg(feature = "system")]
84 #[error("System error: {0}")]
85 System(String),
86
87 #[cfg(feature = "serde")]
89 #[error("Serialization error: {0}")]
90 Serialization(String),
91
92 #[cfg(feature = "includes")]
94 #[error("Include resolution error: {0}")]
95 Include(String),
96
97 #[error("{0}")]
99 Custom(String),
100}
101
102impl Error {
103 #[must_use]
105 pub fn parse(message: impl Into<String>, line: usize, col: usize) -> Self {
106 Self::Parse {
107 message: message.into(),
108 line,
109 col,
110 snippet: None,
111 help: None,
112 }
113 }
114
115 #[must_use]
117 pub fn parse_with_context(
118 message: impl Into<String>,
119 line: usize,
120 col: usize,
121 snippet: impl Into<String>,
122 help: impl Into<String>,
123 ) -> Self {
124 Self::Parse {
125 message: message.into(),
126 line,
127 col,
128 snippet: Some(snippet.into()),
129 help: Some(help.into()),
130 }
131 }
132
133 #[must_use]
135 pub fn unexpected_eof(expected: impl Into<String>, line: usize) -> Self {
136 Self::UnexpectedEof {
137 expected: expected.into(),
138 line,
139 }
140 }
141
142 #[must_use]
144 pub fn syntax(
145 message: impl Into<String>,
146 line: usize,
147 col: usize,
148 expected: Option<String>,
149 found: Option<String>,
150 ) -> Self {
151 Self::Syntax {
152 message: message.into(),
153 line,
154 col,
155 expected,
156 found,
157 }
158 }
159
160 #[must_use]
162 pub fn invalid_directive(
163 name: impl Into<String>,
164 reason: Option<String>,
165 suggestion: Option<String>,
166 ) -> Self {
167 Self::InvalidDirective {
168 name: name.into(),
169 reason,
170 suggestion,
171 }
172 }
173
174 #[must_use]
176 pub fn custom(message: impl Into<String>) -> Self {
177 Self::Custom(message.into())
178 }
179
180 #[must_use]
182 pub fn message(&self) -> String {
183 match self {
184 Self::Parse { message, .. }
185 | Self::InvalidArgument { message, .. }
186 | Self::Syntax { message, .. } => message.clone(),
187 Self::InvalidDirective { name, .. } => name.clone(),
188 _ => self.to_string(),
189 }
190 }
191
192 #[must_use]
200 pub fn detailed(&self) -> String {
201 match self {
202 Self::Parse {
203 message,
204 line,
205 col,
206 snippet,
207 help,
208 } => format_parse_error(*line, *col, message, snippet.as_deref(), help.as_deref()),
209 Self::Syntax {
210 message,
211 line,
212 col,
213 expected,
214 found,
215 } => format_syntax_error(*line, *col, message, expected.as_deref(), found.as_deref()),
216 Self::UnexpectedEof { expected, line } => {
217 format!("Unexpected end of file at line {line}\nExpected: {expected}")
218 }
219 Self::InvalidDirective {
220 name,
221 reason,
222 suggestion,
223 } => {
224 let mut output = format!("Invalid directive: {name}");
225 if let Some(r) = reason {
226 let _ = write!(output, "\nReason: {r}");
227 }
228 if let Some(s) = suggestion {
229 let _ = write!(output, "\nSuggestion: Try using '{s}' instead");
230 }
231 output
232 }
233 _ => self.to_string(),
234 }
235 }
236
237 #[must_use]
239 pub fn short(&self) -> String {
240 match self {
241 Self::Parse {
242 message, line, col, ..
243 }
244 | Self::Syntax {
245 message, line, col, ..
246 } => {
247 format!("line {line}:{col}: {message}")
248 }
249 _ => self.to_string(),
250 }
251 }
252}
253
254fn format_parse_error(
256 line: usize,
257 col: usize,
258 message: &str,
259 snippet: Option<&str>,
260 help: Option<&str>,
261) -> String {
262 let mut output = format!("Parse error at line {line}, column {col}: {message}");
263
264 if let Some(snippet) = snippet {
265 let _ = writeln!(output, "\n");
266 let _ = writeln!(output, "{snippet}");
267 let pointer = format!("{}^", " ".repeat(col.saturating_sub(1)));
269 let _ = writeln!(output, "{pointer}");
270 }
271
272 if let Some(help) = help {
273 let _ = writeln!(output, "\nHelp: {help}");
274 }
275
276 output
277}
278
279fn format_syntax_error(
281 line: usize,
282 col: usize,
283 message: &str,
284 expected: Option<&str>,
285 found: Option<&str>,
286) -> String {
287 let mut output = format!("Syntax error at line {line}, column {col}: {message}");
288
289 if let Some(exp) = expected {
290 let _ = write!(output, "\nExpected: {exp}");
291 }
292
293 if let Some(fnd) = found {
294 let _ = write!(output, "\nFound: {fnd}");
295 }
296
297 output
298}
299
300#[cfg(feature = "serde")]
301impl From<serde_json::Error> for Error {
302 fn from(err: serde_json::Error) -> Self {
303 Self::Serialization(err.to_string())
304 }
305}
306
307#[cfg(feature = "serde")]
308impl From<serde_yaml::Error> for Error {
309 fn from(err: serde_yaml::Error) -> Self {
310 Self::Serialization(err.to_string())
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn test_parse_error() {
320 let err = Error::parse("unexpected token", 10, 5);
321 assert!(err.to_string().contains("line 10"));
322 assert!(err.to_string().contains("column 5"));
323 assert_eq!(err.short(), "line 10:5: unexpected token");
324 }
325
326 #[test]
327 fn test_parse_error_with_context() {
328 let err = Error::parse_with_context(
329 "unexpected semicolon",
330 2,
331 10,
332 "server { listen 80;; }",
333 "Remove the extra semicolon",
334 );
335 let detailed = err.detailed();
336 assert!(detailed.contains("line 2"));
337 assert!(detailed.contains("server { listen 80;; }"));
338 assert!(detailed.contains('^'));
339 assert!(detailed.contains("Help: Remove the extra semicolon"));
340 }
341
342 #[test]
343 fn test_syntax_error() {
344 let err = Error::syntax(
345 "invalid token",
346 5,
347 12,
348 Some("';' or '{'".to_string()),
349 Some("'@'".to_string()),
350 );
351 let detailed = err.detailed();
352 assert!(detailed.contains("Syntax error"));
353 assert!(detailed.contains("Expected: ';' or '{'"));
354 assert!(detailed.contains("Found: '@'"));
355 }
356
357 #[test]
358 fn test_unexpected_eof() {
359 let err = Error::unexpected_eof("closing brace '}'", 100);
360 assert!(err.to_string().contains("Unexpected end of input"));
361 let detailed = err.detailed();
362 assert!(detailed.contains("line 100"));
363 assert!(detailed.contains("Expected: closing brace '}'"));
364 }
365
366 #[test]
367 fn test_invalid_directive() {
368 let err = Error::invalid_directive(
369 "liste",
370 Some("Unknown directive".to_string()),
371 Some("listen".to_string()),
372 );
373 let detailed = err.detailed();
374 assert!(detailed.contains("Invalid directive: liste"));
375 assert!(detailed.contains("Reason: Unknown directive"));
376 assert!(detailed.contains("Try using 'listen' instead"));
377 }
378
379 #[test]
380 fn test_custom_error() {
381 let err = Error::custom("something went wrong");
382 assert_eq!(err.message(), "something went wrong");
383 }
384
385 #[test]
386 fn test_error_formatting() {
387 let err = Error::parse_with_context(
388 "missing semicolon",
389 3,
390 20,
391 "server { listen 80 }",
392 "Add a semicolon after '80'",
393 );
394
395 let detailed = err.detailed();
396 assert!(detailed.contains("line 3"));
398 assert!(detailed.contains("server { listen 80 }"));
400 assert!(detailed.contains('^'));
402 assert!(detailed.contains("Help:"));
404 }
405}