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/// Stable identifier for a parsed source.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
132pub struct SourceId(usize);
133
134impl SourceId {
135    /// Create a source identifier from its document-local index.
136    #[must_use]
137    pub const fn new(index: usize) -> Self {
138        Self(index)
139    }
140
141    /// Return the document-local source index.
142    #[must_use]
143    pub const fn index(self) -> usize {
144        self.0
145    }
146}
147
148/// Byte and line/column location for source-backed items.
149///
150/// Byte offsets are zero-based and half-open: `byte_start..byte_end`.
151/// Lines and columns are one-based. End line and column identify the position
152/// immediately after the final character in the span, so an empty span has the
153/// same start and end position.
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub struct SourceSpan {
156    /// Source identifier, when the span came from a named parsed source.
157    pub source: Option<SourceId>,
158    /// Byte offset where the item starts.
159    pub byte_start: usize,
160    /// Byte offset where the item ends.
161    pub byte_end: usize,
162    /// Line number where the item starts (1-indexed).
163    pub line: usize,
164    /// Column number where the item starts (1-indexed).
165    pub column: usize,
166    /// Line number immediately after the item ends (1-indexed).
167    pub end_line: usize,
168    /// Column number immediately after the item ends (1-indexed).
169    pub end_column: usize,
170}
171
172impl SourceSpan {
173    /// Create a new source span.
174    #[must_use]
175    pub const fn new(byte_start: usize, byte_end: usize, line: usize, column: usize) -> Self {
176        Self {
177            source: None,
178            byte_start,
179            byte_end,
180            line,
181            column,
182            end_line: line,
183            end_column: column,
184        }
185    }
186
187    /// Create a new source span with explicit start and end positions.
188    #[must_use]
189    pub const fn with_end(
190        byte_start: usize,
191        byte_end: usize,
192        line: usize,
193        column: usize,
194        end_line: usize,
195        end_column: usize,
196    ) -> Self {
197        Self {
198            source: None,
199            byte_start,
200            byte_end,
201            line,
202            column,
203            end_line,
204            end_column,
205        }
206    }
207
208    /// Return this span associated with a source identifier.
209    #[must_use]
210    pub const fn with_source(mut self, source: SourceId) -> Self {
211        self.source = Some(source);
212        self
213    }
214
215    /// Return the byte length covered by this span.
216    #[must_use]
217    pub const fn len(self) -> usize {
218        self.byte_end - self.byte_start
219    }
220
221    /// Return true when the span is empty.
222    #[must_use]
223    pub const fn is_empty(self) -> bool {
224        self.byte_start == self.byte_end
225    }
226}