use thiserror::Error;
#[derive(Debug, Clone, Error, PartialEq)]
pub enum FilterError {
#[error("Syntax error at position {position} (line {line}, column {column}): {message}")]
SyntaxError {
position: usize,
line: usize,
column: usize,
message: String,
suggestion: Option<String>,
},
#[error("Unexpected end of input at position {position}: expected {expected}")]
UnexpectedEof {
position: usize,
expected: String,
},
#[error("Invalid character '{char}' at position {position}")]
InvalidChar {
char: char,
position: usize,
},
#[error("Unclosed string starting at position {position}")]
UnclosedString {
position: usize,
},
#[error("Unclosed parenthesis at position {position}")]
UnclosedParen {
position: usize,
},
#[error("Invalid escape sequence '\\{char}' at position {position}")]
InvalidEscape {
char: char,
position: usize,
},
#[error("Invalid number '{value}' at position {position}")]
InvalidNumber {
value: String,
position: usize,
},
#[error("Type mismatch: expected {expected}, got {actual} for field '{field}'")]
TypeMismatch {
field: String,
expected: String,
actual: String,
},
#[error("Cannot compare {left_type} with {right_type}")]
IncompatibleTypes {
left_type: String,
right_type: String,
},
#[error("Operator '{operator}' is not valid for type '{value_type}'")]
InvalidOperatorForType {
operator: String,
value_type: String,
},
#[error("Unknown field '{field}'")]
UnknownField {
field: String,
},
#[error("Division by zero")]
DivisionByZero,
#[error("Null value encountered for field '{field}' in non-nullable context")]
NullValue {
field: String,
},
#[error("Array index {index} out of bounds (length: {length})")]
IndexOutOfBounds {
index: usize,
length: usize,
},
#[error("Invalid expression: {message}")]
InvalidExpression {
message: String,
},
#[error("Nesting too deep (max {max_depth} levels, found {actual_depth})")]
NestingTooDeep {
max_depth: usize,
actual_depth: usize,
},
#[error("Expression too complex (max {max_nodes} nodes, found {actual_nodes})")]
ExpressionTooComplex {
max_nodes: usize,
actual_nodes: usize,
},
#[error("Filter expression too long (max {max_length} bytes, found {actual_length})")]
InputTooLong {
max_length: usize,
actual_length: usize,
},
#[error("Array literal too large (max {max_elements} elements, found {actual_elements})")]
ArrayTooLarge {
max_elements: usize,
actual_elements: usize,
},
#[error("Invalid filter strategy: {0}")]
InvalidStrategy(String),
}
impl FilterError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
FilterError::SyntaxError { .. } => "E001",
FilterError::UnexpectedEof { .. } => "E002",
FilterError::InvalidChar { .. } => "E003",
FilterError::UnclosedString { .. } => "E004",
FilterError::UnclosedParen { .. } => "E005",
FilterError::InvalidEscape { .. } => "E006",
FilterError::InvalidNumber { .. } => "E007",
FilterError::TypeMismatch { .. } => "E101",
FilterError::IncompatibleTypes { .. } => "E102",
FilterError::InvalidOperatorForType { .. } => "E103",
FilterError::UnknownField { .. } => "E105",
FilterError::DivisionByZero => "E201",
FilterError::NullValue { .. } => "E202",
FilterError::IndexOutOfBounds { .. } => "E203",
FilterError::InvalidExpression { .. } => "E204",
FilterError::NestingTooDeep { .. } => "E301",
FilterError::ExpressionTooComplex { .. } => "E302",
FilterError::InputTooLong { .. } => "E303",
FilterError::ArrayTooLarge { .. } => "E304",
FilterError::InvalidStrategy(_) => "E401",
}
}
#[must_use]
pub fn is_syntax_error(&self) -> bool {
matches!(
self,
FilterError::SyntaxError { .. }
| FilterError::UnexpectedEof { .. }
| FilterError::InvalidChar { .. }
| FilterError::UnclosedString { .. }
| FilterError::UnclosedParen { .. }
| FilterError::InvalidEscape { .. }
| FilterError::InvalidNumber { .. }
)
}
#[must_use]
pub fn is_type_error(&self) -> bool {
matches!(
self,
FilterError::TypeMismatch { .. }
| FilterError::IncompatibleTypes { .. }
| FilterError::InvalidOperatorForType { .. }
| FilterError::UnknownField { .. }
)
}
#[must_use]
pub fn is_evaluation_error(&self) -> bool {
matches!(
self,
FilterError::DivisionByZero
| FilterError::NullValue { .. }
| FilterError::IndexOutOfBounds { .. }
| FilterError::InvalidExpression { .. }
)
}
#[must_use]
pub fn is_limit_error(&self) -> bool {
matches!(
self,
FilterError::NestingTooDeep { .. }
| FilterError::ExpressionTooComplex { .. }
| FilterError::InputTooLong { .. }
| FilterError::ArrayTooLarge { .. }
)
}
#[must_use]
pub fn position(&self) -> Option<(usize, usize, usize)> {
match self {
FilterError::SyntaxError {
position,
line,
column,
..
} => Some((*position, *line, *column)),
FilterError::UnexpectedEof { position, .. }
| FilterError::InvalidChar { position, .. }
| FilterError::UnclosedString { position }
| FilterError::UnclosedParen { position }
| FilterError::InvalidEscape { position, .. }
| FilterError::InvalidNumber { position, .. } => Some((*position, 1, *position + 1)),
_ => Option::None,
}
}
#[must_use]
pub fn suggestion(&self) -> Option<String> {
match self {
FilterError::SyntaxError { suggestion, .. } => suggestion.clone(),
FilterError::UnclosedString { .. } => {
Some("Did you forget the closing quote?".to_string())
}
FilterError::UnclosedParen { .. } => {
Some("Did you forget the closing parenthesis?".to_string())
}
FilterError::InvalidEscape { char, .. } => Some(format!(
"Valid escape sequences are: \\\" \\\\ \\n \\r \\t. '\\{char}' is not valid."
)),
FilterError::TypeMismatch {
field,
expected,
actual,
} => Some(format!(
"Field '{field}' is of type '{actual}', but '{expected}' was expected. \
Check that you're using the right operator for this field type."
)),
FilterError::UnknownField { field } => Some(format!(
"Field '{field}' does not exist. Check the field name for typos."
)),
FilterError::NestingTooDeep { max_depth, .. } => Some(format!(
"Simplify your filter expression. Maximum nesting depth is {max_depth}."
)),
FilterError::ArrayTooLarge { max_elements, .. } => Some(format!(
"Use smaller arrays in IN/ANY/ALL operators. Maximum is {max_elements} elements."
)),
_ => Option::None,
}
}
}
pub const MAX_NESTING_DEPTH: usize = 50;
pub const MAX_EXPRESSION_NODES: usize = 1000;
pub const MAX_INPUT_LENGTH: usize = 65536;
pub const MAX_ARRAY_ELEMENTS: usize = 1000;
#[cfg(test)]
#[allow(clippy::unreadable_literal)] mod tests {
use super::*;
#[test]
fn test_syntax_error_codes() {
assert_eq!(
FilterError::SyntaxError {
position: 0,
line: 1,
column: 1,
message: "test".to_string(),
suggestion: None
}
.code(),
"E001"
);
assert_eq!(
FilterError::UnexpectedEof {
position: 0,
expected: "value".to_string()
}
.code(),
"E002"
);
assert_eq!(
FilterError::InvalidChar {
char: '@',
position: 0
}
.code(),
"E003"
);
assert_eq!(FilterError::UnclosedString { position: 0 }.code(), "E004");
assert_eq!(FilterError::UnclosedParen { position: 0 }.code(), "E005");
assert_eq!(
FilterError::InvalidEscape {
char: 'x',
position: 0
}
.code(),
"E006"
);
assert_eq!(
FilterError::InvalidNumber {
value: "1.2.3".to_string(),
position: 0
}
.code(),
"E007"
);
}
#[test]
fn test_type_error_codes() {
assert_eq!(
FilterError::TypeMismatch {
field: "f".to_string(),
expected: "int".to_string(),
actual: "string".to_string()
}
.code(),
"E101"
);
assert_eq!(
FilterError::IncompatibleTypes {
left_type: "int".to_string(),
right_type: "string".to_string()
}
.code(),
"E102"
);
assert_eq!(
FilterError::InvalidOperatorForType {
operator: "<".to_string(),
value_type: "boolean".to_string()
}
.code(),
"E103"
);
assert_eq!(
FilterError::UnknownField {
field: "x".to_string()
}
.code(),
"E105"
);
}
#[test]
fn test_evaluation_error_codes() {
assert_eq!(FilterError::DivisionByZero.code(), "E201");
assert_eq!(
FilterError::NullValue {
field: "x".to_string()
}
.code(),
"E202"
);
assert_eq!(
FilterError::IndexOutOfBounds {
index: 5,
length: 3
}
.code(),
"E203"
);
}
#[test]
fn test_limit_error_codes() {
assert_eq!(
FilterError::NestingTooDeep {
max_depth: 50,
actual_depth: 100
}
.code(),
"E301"
);
assert_eq!(
FilterError::ExpressionTooComplex {
max_nodes: 1000,
actual_nodes: 2000
}
.code(),
"E302"
);
assert_eq!(
FilterError::InputTooLong {
max_length: 65536,
actual_length: 100000
}
.code(),
"E303"
);
assert_eq!(
FilterError::ArrayTooLarge {
max_elements: 1000,
actual_elements: 2000
}
.code(),
"E304"
);
}
#[test]
fn test_is_syntax_error() {
assert!(FilterError::SyntaxError {
position: 0,
line: 1,
column: 1,
message: "test".to_string(),
suggestion: None
}
.is_syntax_error());
assert!(FilterError::UnclosedString { position: 0 }.is_syntax_error());
assert!(!FilterError::DivisionByZero.is_syntax_error());
}
#[test]
fn test_is_type_error() {
assert!(FilterError::TypeMismatch {
field: "f".to_string(),
expected: "int".to_string(),
actual: "string".to_string()
}
.is_type_error());
assert!(FilterError::UnknownField {
field: "x".to_string()
}
.is_type_error());
assert!(!FilterError::DivisionByZero.is_type_error());
}
#[test]
fn test_is_evaluation_error() {
assert!(FilterError::DivisionByZero.is_evaluation_error());
assert!(FilterError::NullValue {
field: "x".to_string()
}
.is_evaluation_error());
assert!(!FilterError::UnclosedString { position: 0 }.is_evaluation_error());
}
#[test]
fn test_is_limit_error() {
assert!(FilterError::NestingTooDeep {
max_depth: 50,
actual_depth: 100
}
.is_limit_error());
assert!(FilterError::ArrayTooLarge {
max_elements: 1000,
actual_elements: 2000
}
.is_limit_error());
assert!(!FilterError::DivisionByZero.is_limit_error());
}
#[test]
fn test_position_syntax_error() {
let error = FilterError::SyntaxError {
position: 10,
line: 2,
column: 5,
message: "test".to_string(),
suggestion: None,
};
assert_eq!(error.position(), Some((10, 2, 5)));
}
#[test]
fn test_position_other_syntax_errors() {
assert_eq!(
FilterError::UnclosedString { position: 5 }.position(),
Some((5, 1, 6))
);
assert_eq!(
FilterError::InvalidChar {
char: '@',
position: 3
}
.position(),
Some((3, 1, 4))
);
}
#[test]
fn test_position_non_positional() {
assert_eq!(FilterError::DivisionByZero.position(), Option::None);
assert_eq!(
FilterError::TypeMismatch {
field: "f".to_string(),
expected: "int".to_string(),
actual: "string".to_string()
}
.position(),
Option::None
);
}
#[test]
fn test_suggestion_unclosed_string() {
let error = FilterError::UnclosedString { position: 0 };
assert!(error.suggestion().is_some());
assert!(error.suggestion().unwrap().contains("closing quote"));
}
#[test]
fn test_suggestion_unclosed_paren() {
let error = FilterError::UnclosedParen { position: 0 };
assert!(error.suggestion().is_some());
assert!(error.suggestion().unwrap().contains("closing parenthesis"));
}
#[test]
fn test_suggestion_invalid_escape() {
let error = FilterError::InvalidEscape {
char: 'x',
position: 0,
};
let suggestion = error.suggestion().unwrap();
assert!(suggestion.contains("\\\""));
assert!(suggestion.contains("\\\\"));
}
#[test]
fn test_suggestion_type_mismatch() {
let error = FilterError::TypeMismatch {
field: "price".to_string(),
expected: "integer".to_string(),
actual: "string".to_string(),
};
let suggestion = error.suggestion().unwrap();
assert!(suggestion.contains("price"));
assert!(suggestion.contains("string"));
assert!(suggestion.contains("integer"));
}
#[test]
fn test_suggestion_unknown_field() {
let error = FilterError::UnknownField {
field: "categry".to_string(),
};
let suggestion = error.suggestion().unwrap();
assert!(suggestion.contains("categry"));
assert!(suggestion.contains("typos"));
}
#[test]
fn test_display_syntax_error() {
let error = FilterError::SyntaxError {
position: 10,
line: 1,
column: 11,
message: "Expected operator".to_string(),
suggestion: None,
};
let display = format!("{error}");
assert!(display.contains("position 10"));
assert!(display.contains("line 1"));
assert!(display.contains("column 11"));
assert!(display.contains("Expected operator"));
}
#[test]
fn test_display_type_mismatch() {
let error = FilterError::TypeMismatch {
field: "price".to_string(),
expected: "integer".to_string(),
actual: "string".to_string(),
};
let display = format!("{error}");
assert!(display.contains("Type mismatch"));
assert!(display.contains("price"));
assert!(display.contains("integer"));
assert!(display.contains("string"));
}
#[test]
fn test_constants() {
assert_eq!(MAX_NESTING_DEPTH, 50);
assert_eq!(MAX_EXPRESSION_NODES, 1000);
assert_eq!(MAX_INPUT_LENGTH, 65536);
assert_eq!(MAX_ARRAY_ELEMENTS, 1000);
}
#[test]
fn test_clone() {
let error = FilterError::SyntaxError {
position: 10,
line: 1,
column: 11,
message: "test".to_string(),
suggestion: Some("hint".to_string()),
};
let cloned = error.clone();
assert_eq!(error, cloned);
}
#[test]
fn test_partial_eq() {
let error1 = FilterError::InvalidChar {
char: '@',
position: 5,
};
let error2 = FilterError::InvalidChar {
char: '@',
position: 5,
};
let error3 = FilterError::InvalidChar {
char: '#',
position: 5,
};
assert_eq!(error1, error2);
assert_ne!(error1, error3);
}
}