use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, ValidationError>;
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("TTL parse error in {file}: {message}")]
ParseError {
file: PathBuf,
message: String,
line: usize,
column: usize,
},
#[error("Failed to load SHACL shapes from {file}: {reason}")]
ShapeLoadError {
file: PathBuf,
reason: String,
},
#[error("SPARQL query execution failed: {message}")]
SparqlError {
message: String,
query: String,
},
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Oxigraph error: {0}")]
OxigraphError(String),
#[error("Invalid SHACL constraint: {message}")]
InvalidConstraint {
message: String,
shape_iri: String,
},
#[error("Validation timeout after {duration_ms}ms (limit: {limit_ms}ms)")]
TimeoutError {
duration_ms: u64,
limit_ms: u64,
},
#[error("Syntax validation failed in {file}:{line}:{column} [{language}]: {error}")]
SyntaxValidationFailed {
file: String,
line: usize,
column: usize,
language: String,
error: String,
},
#[error("General validation error: {message}")]
General {
message: String,
},
}
impl ValidationError {
pub fn parse_error(
file: impl Into<PathBuf>, message: impl Into<String>, line: usize, column: usize,
) -> Self {
ValidationError::ParseError {
file: file.into(),
message: message.into(),
line,
column,
}
}
pub fn shape_load_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
ValidationError::ShapeLoadError {
file: file.into(),
reason: reason.into(),
}
}
pub fn sparql_error(message: impl Into<String>, query: impl Into<String>) -> Self {
ValidationError::SparqlError {
message: message.into(),
query: query.into(),
}
}
pub fn invalid_constraint(message: impl Into<String>, shape_iri: impl Into<String>) -> Self {
ValidationError::InvalidConstraint {
message: message.into(),
shape_iri: shape_iri.into(),
}
}
pub fn timeout_error(duration_ms: u64, limit_ms: u64) -> Self {
ValidationError::TimeoutError {
duration_ms,
limit_ms,
}
}
pub fn timeout(context: impl Into<String>, limit_ms: u64) -> Self {
ValidationError::OxigraphError(format!(
"{} exceeded timeout of {}ms",
context.into(),
limit_ms
))
}
pub fn invalid_query(rule_id: &str, reason: &str) -> Self {
ValidationError::SparqlError {
message: format!("Invalid query for rule {}: {}", rule_id, reason),
query: String::new(),
}
}
pub fn query_execution(rule_id: &str, error: &str) -> Self {
ValidationError::SparqlError {
message: format!("Query execution failed for rule {}: {}", rule_id, error),
query: String::new(),
}
}
pub fn syntax_validation_failed(
file: impl Into<String>, line: usize, column: usize, language: impl Into<String>,
error: impl Into<String>,
) -> Self {
ValidationError::SyntaxValidationFailed {
file: file.into(),
line,
column,
language: language.into(),
error: error.into(),
}
}
}
impl From<oxigraph::store::LoaderError> for ValidationError {
fn from(err: oxigraph::store::LoaderError) -> Self {
ValidationError::OxigraphError(err.to_string())
}
}
impl From<oxigraph::sparql::QueryEvaluationError> for ValidationError {
fn from(err: oxigraph::sparql::QueryEvaluationError) -> Self {
ValidationError::SparqlError {
message: err.to_string(),
query: String::new(), }
}
}
impl From<crate::validation::input::InputValidationError> for ValidationError {
fn from(error: crate::validation::input::InputValidationError) -> Self {
ValidationError::General {
message: format!("Input validation error: {}", error),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_construction() {
let err =
ValidationError::parse_error(PathBuf::from("test.ttl"), "Unexpected token", 42, 15);
match err {
ValidationError::ParseError {
file,
message,
line,
column,
} => {
assert_eq!(file, PathBuf::from("test.ttl"));
assert_eq!(message, "Unexpected token");
assert_eq!(line, 42);
assert_eq!(column, 15);
}
_ => panic!("Expected ParseError"),
}
}
#[test]
fn test_shape_load_error_display() {
let err = ValidationError::shape_load_error(PathBuf::from("shapes.ttl"), "File not found");
let display = format!("{}", err);
assert!(display.contains("shapes.ttl"));
assert!(display.contains("File not found"));
}
#[test]
fn test_sparql_error_construction() {
let err = ValidationError::sparql_error(
"Unknown prefix 'sk'",
"SELECT ?s WHERE { ?s a sk:UserStory }",
);
match err {
ValidationError::SparqlError { message, query } => {
assert_eq!(message, "Unknown prefix 'sk'");
assert!(query.contains("sk:UserStory"));
}
_ => panic!("Expected SparqlError"),
}
}
#[test]
fn test_invalid_constraint_error() {
let err = ValidationError::invalid_constraint("Unsupported constraint", ":UserStoryShape");
match err {
ValidationError::InvalidConstraint { message, shape_iri } => {
assert_eq!(message, "Unsupported constraint");
assert_eq!(shape_iri, ":UserStoryShape");
}
_ => panic!("Expected InvalidConstraint"),
}
}
#[test]
fn test_timeout_error() {
let err = ValidationError::timeout_error(35000, 30000);
match err {
ValidationError::TimeoutError {
duration_ms,
limit_ms,
} => {
assert_eq!(duration_ms, 35000);
assert_eq!(limit_ms, 30000);
}
_ => panic!("Expected TimeoutError"),
}
}
#[test]
fn test_result_type_alias() {
fn returns_result() -> Result<i32> {
Ok(42)
}
assert_eq!(returns_result().unwrap(), 42);
}
#[test]
fn test_error_propagation() {
fn inner() -> Result<()> {
Err(ValidationError::parse_error(
"test.ttl",
"Syntax error",
10,
5,
))
}
fn outer() -> Result<()> {
inner()?; Ok(())
}
assert!(outer().is_err());
}
}