use serde::{Deserialize, Serialize};
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
#[error("Tokenization error at line {line}, column {column}: {message}")]
Tokenize {
message: String,
line: usize,
column: usize,
start: usize,
end: usize,
},
#[error("Parse error at line {line}, column {column}: {message}")]
Parse {
message: String,
line: usize,
column: usize,
start: usize,
end: usize,
},
#[error("Generation error: {0}")]
Generate(String),
#[error("Unsupported: {feature} is not supported in {dialect}")]
Unsupported { feature: String, dialect: String },
#[error("Syntax error at line {line}, column {column}: {message}")]
Syntax {
message: String,
line: usize,
column: usize,
start: usize,
end: usize,
},
#[error("Internal error: {0}")]
Internal(String),
}
impl Error {
pub fn tokenize(
message: impl Into<String>,
line: usize,
column: usize,
start: usize,
end: usize,
) -> Self {
Error::Tokenize {
message: message.into(),
line,
column,
start,
end,
}
}
pub fn parse(
message: impl Into<String>,
line: usize,
column: usize,
start: usize,
end: usize,
) -> Self {
Error::Parse {
message: message.into(),
line,
column,
start,
end,
}
}
pub fn line(&self) -> Option<usize> {
match self {
Error::Tokenize { line, .. }
| Error::Parse { line, .. }
| Error::Syntax { line, .. } => Some(*line),
_ => None,
}
}
pub fn column(&self) -> Option<usize> {
match self {
Error::Tokenize { column, .. }
| Error::Parse { column, .. }
| Error::Syntax { column, .. } => Some(*column),
_ => None,
}
}
pub fn start(&self) -> Option<usize> {
match self {
Error::Tokenize { start, .. }
| Error::Parse { start, .. }
| Error::Syntax { start, .. } => Some(*start),
_ => None,
}
}
pub fn end(&self) -> Option<usize> {
match self {
Error::Tokenize { end, .. } | Error::Parse { end, .. } | Error::Syntax { end, .. } => {
Some(*end)
}
_ => None,
}
}
pub fn generate(message: impl Into<String>) -> Self {
Error::Generate(message.into())
}
pub fn unsupported(feature: impl Into<String>, dialect: impl Into<String>) -> Self {
Error::Unsupported {
feature: feature.into(),
dialect: dialect.into(),
}
}
pub fn syntax(
message: impl Into<String>,
line: usize,
column: usize,
start: usize,
end: usize,
) -> Self {
Error::Syntax {
message: message.into(),
line,
column,
start,
end,
}
}
pub fn internal(message: impl Into<String>) -> Self {
Error::Internal(message.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ValidationSeverity {
Error,
Warning,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub message: String,
pub line: Option<usize>,
pub column: Option<usize>,
pub severity: ValidationSeverity,
pub code: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end: Option<usize>,
}
impl ValidationError {
pub fn error(message: impl Into<String>, code: impl Into<String>) -> Self {
Self {
message: message.into(),
line: None,
column: None,
severity: ValidationSeverity::Error,
code: code.into(),
start: None,
end: None,
}
}
pub fn warning(message: impl Into<String>, code: impl Into<String>) -> Self {
Self {
message: message.into(),
line: None,
column: None,
severity: ValidationSeverity::Warning,
code: code.into(),
start: None,
end: None,
}
}
pub fn with_line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn with_location(mut self, line: usize, column: usize) -> Self {
self.line = Some(line);
self.column = Some(column);
self
}
pub fn with_span(mut self, start: Option<usize>, end: Option<usize>) -> Self {
self.start = start;
self.end = end;
self
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
}
}
pub fn with_errors(errors: Vec<ValidationError>) -> Self {
let has_errors = errors
.iter()
.any(|e| e.severity == ValidationSeverity::Error);
Self {
valid: !has_errors,
errors,
}
}
pub fn add_error(&mut self, error: ValidationError) {
if error.severity == ValidationSeverity::Error {
self.valid = false;
}
self.errors.push(error);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_has_position() {
let err = Error::parse("test message", 5, 10, 20, 25);
assert_eq!(err.line(), Some(5));
assert_eq!(err.column(), Some(10));
assert_eq!(err.start(), Some(20));
assert_eq!(err.end(), Some(25));
assert!(err.to_string().contains("line 5"));
assert!(err.to_string().contains("column 10"));
assert!(err.to_string().contains("test message"));
}
#[test]
fn test_tokenize_error_has_position() {
let err = Error::tokenize("bad token", 3, 7, 15, 20);
assert_eq!(err.line(), Some(3));
assert_eq!(err.column(), Some(7));
assert_eq!(err.start(), Some(15));
assert_eq!(err.end(), Some(20));
}
#[test]
fn test_generate_error_has_no_position() {
let err = Error::generate("gen error");
assert_eq!(err.line(), None);
assert_eq!(err.column(), None);
assert_eq!(err.start(), None);
assert_eq!(err.end(), None);
}
#[test]
fn test_parse_error_position_from_parser() {
use crate::dialects::{Dialect, DialectType};
let d = Dialect::get(DialectType::Generic);
let result = d.parse("SELECT 1 + 2)");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.line().is_some(),
"Parse error should have line: {:?}",
err
);
assert!(
err.column().is_some(),
"Parse error should have column: {:?}",
err
);
assert_eq!(err.line(), Some(1));
}
#[test]
fn test_parse_error_has_span_offsets() {
use crate::dialects::{Dialect, DialectType};
let d = Dialect::get(DialectType::Generic);
let result = d.parse("SELECT 1 + 2)");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.start().is_some(),
"Parse error should have start offset: {:?}",
err
);
assert!(
err.end().is_some(),
"Parse error should have end offset: {:?}",
err
);
assert_eq!(err.start(), Some(12));
assert_eq!(err.end(), Some(13));
}
#[test]
fn test_validation_error_with_span() {
let err = ValidationError::error("test", "E001")
.with_location(1, 5)
.with_span(Some(4), Some(10));
assert_eq!(err.start, Some(4));
assert_eq!(err.end, Some(10));
assert_eq!(err.line, Some(1));
assert_eq!(err.column, Some(5));
}
}