Skip to main content

bibtex_parser/
error.rs

1//! Error types for the bibtex-parser crate
2
3use std::fmt;
4use thiserror::Error;
5
6/// Result type for bibtex-parser operations
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// The main error type for bibtex-parser
10#[derive(Error, Debug)]
11pub enum Error {
12    /// Parse error with location information
13    #[error("Parse error at line {line}, column {column}: {message}")]
14    ParseError {
15        /// Line number (1-indexed)
16        line: usize,
17        /// Column number (1-indexed)
18        column: usize,
19        /// Error message
20        message: String,
21        /// Optional source snippet
22        snippet: Option<String>,
23    },
24
25    /// Undefined string variable
26    #[error("Undefined string variable '{0}'")]
27    UndefinedVariable(String),
28
29    /// Circular reference in string variables
30    #[error("Circular reference detected in string variables: {0}")]
31    CircularReference(String),
32
33    /// Invalid entry type
34    #[error("Invalid entry type '{0}'")]
35    InvalidEntryType(String),
36
37    /// Missing required field
38    #[error("Missing required field '{field}' in {entry_type} entry")]
39    MissingRequiredField {
40        /// The entry type
41        entry_type: String,
42        /// The missing field
43        field: String,
44    },
45
46    /// Duplicate entry key
47    #[error("Duplicate entry key '{0}'")]
48    DuplicateKey(String),
49
50    /// Invalid field name
51    #[error("Invalid field name '{0}'")]
52    InvalidFieldName(String),
53
54    /// IO error
55    #[error("IO error: {0}")]
56    IoError(#[from] std::io::Error),
57
58    /// Generic parse error from winnow
59    #[error("Parse error: {0}")]
60    WinnowError(String),
61}
62
63/// Parse context for better error messages
64#[derive(Debug, Clone)]
65pub struct ParseContext {
66    /// The full input string being parsed
67    pub input: String,
68    /// Current line number (1-indexed)
69    pub line: usize,
70    /// Current column number (1-indexed)
71    pub column: usize,
72}
73
74impl ParseContext {
75    /// Create a new parse context
76    #[must_use]
77    pub fn new(input: &str) -> Self {
78        Self {
79            input: input.to_string(),
80            line: 1,
81            column: 1,
82        }
83    }
84
85    /// Get a snippet of the input around the current position
86    #[must_use]
87    pub fn snippet(&self, pos: usize, context_size: usize) -> String {
88        let start = pos.saturating_sub(context_size);
89        let end = (pos + context_size).min(self.input.len());
90        let snippet = &self.input[start..end];
91        let relative_pos = pos - start;
92        format!("{}\n{}^", snippet, " ".repeat(relative_pos))
93    }
94
95    /// Update position based on consumed input
96    pub fn advance(&mut self, consumed: &str) {
97        for ch in consumed.chars() {
98            if ch == '\n' {
99                self.line += 1;
100                self.column = 1;
101            } else {
102                self.column += 1;
103            }
104        }
105    }
106}
107
108/// Convert winnow errors to our error type
109impl From<winnow::error::ContextError> for Error {
110    fn from(err: winnow::error::ContextError) -> Self {
111        Self::WinnowError(err.to_string())
112    }
113}
114
115/// Location information for errors
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct Location {
118    /// Line number (1-indexed)
119    pub line: usize,
120    /// Column number (1-indexed)
121    pub column: usize,
122}
123
124impl fmt::Display for Location {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "{}:{}", self.line, self.column)
127    }
128}
129
130/// Byte and line/column location for source-backed items.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct SourceSpan {
133    /// Byte offset where the item starts.
134    pub byte_start: usize,
135    /// Byte offset where the item ends.
136    pub byte_end: usize,
137    /// Line number where the item starts (1-indexed).
138    pub line: usize,
139    /// Column number where the item starts (1-indexed).
140    pub column: usize,
141}
142
143impl SourceSpan {
144    /// Create a new source span.
145    #[must_use]
146    pub const fn new(byte_start: usize, byte_end: usize, line: usize, column: usize) -> Self {
147        Self {
148            byte_start,
149            byte_end,
150            line,
151            column,
152        }
153    }
154
155    /// Return the byte length covered by this span.
156    #[must_use]
157    pub const fn len(self) -> usize {
158        self.byte_end - self.byte_start
159    }
160
161    /// Return true when the span is empty.
162    #[must_use]
163    pub const fn is_empty(self) -> bool {
164        self.byte_start == self.byte_end
165    }
166}