Skip to main content

nautilus_schema/
error.rs

1//! Error types for schema parsing and validation.
2
3use crate::span::Span;
4use std::fmt;
5
6/// Error type for schema operations.
7#[derive(Debug, Clone, PartialEq)]
8pub enum SchemaError {
9    /// Lexer error with position information.
10    Lexer(String, Span),
11    /// Unterminated string literal.
12    UnterminatedString(Span),
13    /// Invalid number literal.
14    InvalidNumber(String, Span),
15    /// Unexpected character.
16    UnexpectedCharacter(char, Span),
17    /// Parser error.
18    Parse(String, Span),
19    /// Semantic validation error.
20    Validation(String, Span),
21    /// Non-blocking semantic warning.
22    Warning(String, Span),
23    /// Generic error without span.
24    Other(String),
25}
26
27impl SchemaError {
28    /// Format error with source context for display.
29    pub fn format_with_source(&self, source: &str) -> String {
30        match self {
31            SchemaError::Lexer(msg, span)
32            | SchemaError::Parse(msg, span)
33            | SchemaError::Validation(msg, span)
34            | SchemaError::Warning(msg, span) => {
35                let (start_pos, _) = span.to_positions(source);
36                format!("{} at {}", msg, start_pos)
37            }
38            SchemaError::UnterminatedString(span) => {
39                let (start_pos, _) = span.to_positions(source);
40                format!("Unterminated string literal at {}", start_pos)
41            }
42            SchemaError::InvalidNumber(num, span) => {
43                let (start_pos, _) = span.to_positions(source);
44                format!("Invalid number '{}' at {}", num, start_pos)
45            }
46            SchemaError::UnexpectedCharacter(ch, span) => {
47                let (start_pos, _) = span.to_positions(source);
48                format!("Unexpected character '{}' at {}", ch, start_pos)
49            }
50            SchemaError::Other(msg) => msg.clone(),
51        }
52    }
53
54    /// Format error with file path and source context.
55    /// Uses the format `filepath:line:column: message` which is recognized
56    /// as a clickable link by VS Code and other IDEs.
57    pub fn format_with_file(&self, filepath: &str, source: &str) -> String {
58        match self {
59            SchemaError::Lexer(msg, span)
60            | SchemaError::Parse(msg, span)
61            | SchemaError::Validation(msg, span)
62            | SchemaError::Warning(msg, span) => {
63                let (start_pos, _) = span.to_positions(source);
64                format!(
65                    "{}:{}:{}: {}",
66                    filepath, start_pos.line, start_pos.column, msg
67                )
68            }
69            SchemaError::UnterminatedString(span) => {
70                let (start_pos, _) = span.to_positions(source);
71                format!(
72                    "{}:{}:{}: Unterminated string literal",
73                    filepath, start_pos.line, start_pos.column
74                )
75            }
76            SchemaError::InvalidNumber(num, span) => {
77                let (start_pos, _) = span.to_positions(source);
78                format!(
79                    "{}:{}:{}: Invalid number '{}'",
80                    filepath, start_pos.line, start_pos.column, num
81                )
82            }
83            SchemaError::UnexpectedCharacter(ch, span) => {
84                let (start_pos, _) = span.to_positions(source);
85                format!(
86                    "{}:{}:{}: Unexpected character '{}'",
87                    filepath, start_pos.line, start_pos.column, ch
88                )
89            }
90            SchemaError::Other(msg) => msg.clone(),
91        }
92    }
93
94    /// Get the span associated with this error, if any.
95    pub fn span(&self) -> Option<Span> {
96        match self {
97            SchemaError::Lexer(_, span)
98            | SchemaError::Parse(_, span)
99            | SchemaError::Validation(_, span)
100            | SchemaError::Warning(_, span)
101            | SchemaError::UnterminatedString(span)
102            | SchemaError::InvalidNumber(_, span)
103            | SchemaError::UnexpectedCharacter(_, span) => Some(*span),
104            SchemaError::Other(_) => None,
105        }
106    }
107}
108
109impl fmt::Display for SchemaError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            SchemaError::Lexer(msg, _) => write!(f, "Lexer error: {}", msg),
113            SchemaError::UnterminatedString(_) => write!(f, "Unterminated string literal"),
114            SchemaError::InvalidNumber(num, _) => write!(f, "Invalid number: {}", num),
115            SchemaError::UnexpectedCharacter(ch, _) => write!(f, "Unexpected character: '{}'", ch),
116            SchemaError::Parse(msg, _) => write!(f, "Parse error: {}", msg),
117            SchemaError::Validation(msg, _) => write!(f, "Validation error: {}", msg),
118            SchemaError::Warning(msg, _) => write!(f, "Warning: {}", msg),
119            SchemaError::Other(msg) => write!(f, "{}", msg),
120        }
121    }
122}
123
124impl std::error::Error for SchemaError {}
125
126/// Result type alias for schema operations.
127pub type Result<T> = std::result::Result<T, SchemaError>;
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_error_display() {
135        let span = Span::new(0, 5);
136        let err = SchemaError::Lexer("test error".to_string(), span);
137        assert_eq!(err.to_string(), "Lexer error: test error");
138    }
139
140    #[test]
141    fn test_error_span() {
142        let span = Span::new(10, 15);
143        let err = SchemaError::UnexpectedCharacter('#', span);
144        assert_eq!(err.span(), Some(span));
145
146        let err_no_span = SchemaError::Other("generic".to_string());
147        assert_eq!(err_no_span.span(), None);
148    }
149
150    #[test]
151    fn test_format_with_source() {
152        let source = "hello\nworld";
153        let span = Span::new(6, 11); // "world"
154        let err = SchemaError::Lexer("unexpected token".to_string(), span);
155        let formatted = err.format_with_source(source);
156        assert!(formatted.contains("2:1")); // Line 2, column 1
157    }
158
159    #[test]
160    fn test_format_with_file() {
161        let source = "hello\nworld";
162        let span = Span::new(6, 11); // "world"
163        let err = SchemaError::Lexer("unexpected token".to_string(), span);
164        let formatted = err.format_with_file("schema.nautilus", source);
165        assert_eq!(formatted, "schema.nautilus:2:1: unexpected token");
166    }
167}