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 #[error("Network error: {0}")]
103 Network(String),
104
105 #[error("Invalid input: {0}")]
107 InvalidInput(String),
108
109 #[error("Not implemented: {0}")]
111 NotImplemented(String),
112
113 #[error("Feature '{0}' not enabled. Enable it in Cargo.toml")]
115 FeatureNotEnabled(String),
116}
117
118#[cfg(feature = "export-toml")]
119impl From<toml::ser::Error> for Error {
120 fn from(err: toml::ser::Error) -> Self {
121 Self::Io(std::io::Error::new(
122 std::io::ErrorKind::Other,
123 err.to_string(),
124 ))
125 }
126}
127
128impl From<std::fmt::Error> for Error {
129 fn from(err: std::fmt::Error) -> Self {
130 Self::Io(std::io::Error::new(
131 std::io::ErrorKind::Other,
132 err.to_string(),
133 ))
134 }
135}
136
137impl Error {
138 #[must_use]
140 pub fn parse(message: impl Into<String>, line: usize, col: usize) -> Self {
141 Self::Parse {
142 message: message.into(),
143 line,
144 col,
145 snippet: None,
146 help: None,
147 }
148 }
149
150 #[must_use]
152 pub fn parse_with_context(
153 message: impl Into<String>,
154 line: usize,
155 col: usize,
156 snippet: impl Into<String>,
157 help: impl Into<String>,
158 ) -> Self {
159 Self::Parse {
160 message: message.into(),
161 line,
162 col,
163 snippet: Some(snippet.into()),
164 help: Some(help.into()),
165 }
166 }
167
168 #[must_use]
170 pub fn unexpected_eof(expected: impl Into<String>, line: usize) -> Self {
171 Self::UnexpectedEof {
172 expected: expected.into(),
173 line,
174 }
175 }
176
177 #[must_use]
179 pub fn syntax(
180 message: impl Into<String>,
181 line: usize,
182 col: usize,
183 expected: Option<String>,
184 found: Option<String>,
185 ) -> Self {
186 Self::Syntax {
187 message: message.into(),
188 line,
189 col,
190 expected,
191 found,
192 }
193 }
194
195 #[must_use]
197 pub fn invalid_directive(
198 name: impl Into<String>,
199 reason: Option<String>,
200 suggestion: Option<String>,
201 ) -> Self {
202 Self::InvalidDirective {
203 name: name.into(),
204 reason,
205 suggestion,
206 }
207 }
208
209 #[must_use]
211 pub fn custom(message: impl Into<String>) -> Self {
212 Self::Custom(message.into())
213 }
214
215 #[must_use]
217 pub fn message(&self) -> String {
218 match self {
219 Self::Parse { message, .. }
220 | Self::InvalidArgument { message, .. }
221 | Self::Syntax { message, .. } => message.clone(),
222 Self::InvalidDirective { name, .. } => name.clone(),
223 _ => self.to_string(),
224 }
225 }
226
227 #[must_use]
235 pub fn detailed(&self) -> String {
236 match self {
237 Self::Parse {
238 message,
239 line,
240 col,
241 snippet,
242 help,
243 } => format_parse_error(*line, *col, message, snippet.as_deref(), help.as_deref()),
244 Self::Syntax {
245 message,
246 line,
247 col,
248 expected,
249 found,
250 } => format_syntax_error(*line, *col, message, expected.as_deref(), found.as_deref()),
251 Self::UnexpectedEof { expected, line } => {
252 format!("Unexpected end of file at line {line}\nExpected: {expected}")
253 }
254 Self::InvalidDirective {
255 name,
256 reason,
257 suggestion,
258 } => {
259 let mut output = format!("Invalid directive: {name}");
260 if let Some(r) = reason {
261 let _ = write!(output, "\nReason: {r}");
262 }
263 if let Some(s) = suggestion {
264 let _ = write!(output, "\nSuggestion: Try using '{s}' instead");
265 }
266 output
267 }
268 _ => self.to_string(),
269 }
270 }
271
272 #[must_use]
274 pub fn short(&self) -> String {
275 match self {
276 Self::Parse {
277 message, line, col, ..
278 }
279 | Self::Syntax {
280 message, line, col, ..
281 } => {
282 format!("line {line}:{col}: {message}")
283 }
284 _ => self.to_string(),
285 }
286 }
287}
288
289fn format_parse_error(
291 line: usize,
292 col: usize,
293 message: &str,
294 snippet: Option<&str>,
295 help: Option<&str>,
296) -> String {
297 let mut output = format!("Parse error at line {line}, column {col}: {message}");
298
299 if let Some(snippet) = snippet {
300 let _ = writeln!(output, "\n");
301 let _ = writeln!(output, "{snippet}");
302 let pointer = format!("{}^", " ".repeat(col.saturating_sub(1)));
304 let _ = writeln!(output, "{pointer}");
305 }
306
307 if let Some(help) = help {
308 let _ = writeln!(output, "\nHelp: {help}");
309 }
310
311 output
312}
313
314fn format_syntax_error(
316 line: usize,
317 col: usize,
318 message: &str,
319 expected: Option<&str>,
320 found: Option<&str>,
321) -> String {
322 let mut output = format!("Syntax error at line {line}, column {col}: {message}");
323
324 if let Some(exp) = expected {
325 let _ = write!(output, "\nExpected: {exp}");
326 }
327
328 if let Some(fnd) = found {
329 let _ = write!(output, "\nFound: {fnd}");
330 }
331
332 output
333}
334
335#[cfg(feature = "serde")]
336impl From<serde_json::Error> for Error {
337 fn from(err: serde_json::Error) -> Self {
338 Self::Serialization(err.to_string())
339 }
340}
341
342#[cfg(feature = "serde")]
343impl From<serde_yaml::Error> for Error {
344 fn from(err: serde_yaml::Error) -> Self {
345 Self::Serialization(err.to_string())
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_parse_error() {
355 let err = Error::parse("unexpected token", 10, 5);
356 assert!(err.to_string().contains("line 10"));
357 assert!(err.to_string().contains("column 5"));
358 assert_eq!(err.short(), "line 10:5: unexpected token");
359 }
360
361 #[test]
362 fn test_parse_error_with_context() {
363 let err = Error::parse_with_context(
364 "unexpected semicolon",
365 2,
366 10,
367 "server { listen 80;; }",
368 "Remove the extra semicolon",
369 );
370 let detailed = err.detailed();
371 assert!(detailed.contains("line 2"));
372 assert!(detailed.contains("server { listen 80;; }"));
373 assert!(detailed.contains('^'));
374 assert!(detailed.contains("Help: Remove the extra semicolon"));
375 }
376
377 #[test]
378 fn test_syntax_error() {
379 let err = Error::syntax(
380 "invalid token",
381 5,
382 12,
383 Some("';' or '{'".to_string()),
384 Some("'@'".to_string()),
385 );
386 let detailed = err.detailed();
387 assert!(detailed.contains("Syntax error"));
388 assert!(detailed.contains("Expected: ';' or '{'"));
389 assert!(detailed.contains("Found: '@'"));
390 }
391
392 #[test]
393 fn test_unexpected_eof() {
394 let err = Error::unexpected_eof("closing brace '}'", 100);
395 assert!(err.to_string().contains("Unexpected end of input"));
396 let detailed = err.detailed();
397 assert!(detailed.contains("line 100"));
398 assert!(detailed.contains("Expected: closing brace '}'"));
399 }
400
401 #[test]
402 fn test_invalid_directive() {
403 let err = Error::invalid_directive(
404 "liste",
405 Some("Unknown directive".to_string()),
406 Some("listen".to_string()),
407 );
408 let detailed = err.detailed();
409 assert!(detailed.contains("Invalid directive: liste"));
410 assert!(detailed.contains("Reason: Unknown directive"));
411 assert!(detailed.contains("Try using 'listen' instead"));
412 }
413
414 #[test]
415 fn test_custom_error() {
416 let err = Error::custom("something went wrong");
417 assert_eq!(err.message(), "something went wrong");
418 }
419
420 #[test]
421 fn test_error_formatting() {
422 let err = Error::parse_with_context(
423 "missing semicolon",
424 3,
425 20,
426 "server { listen 80 }",
427 "Add a semicolon after '80'",
428 );
429
430 let detailed = err.detailed();
431 assert!(detailed.contains("line 3"));
433 assert!(detailed.contains("server { listen 80 }"));
435 assert!(detailed.contains('^'));
437 assert!(detailed.contains("Help:"));
439 }
440}