#![allow(unused_assignments)]
use miette::Diagnostic;
use thiserror::Error;
pub type SchemaResult<T> = Result<T, SchemaError>;
#[derive(Error, Debug, Diagnostic)]
pub enum SchemaError {
#[error("failed to read file: {path}")]
#[diagnostic(code(prax::schema::io_error))]
IoError {
path: String,
#[source]
source: std::io::Error,
},
#[error("syntax error in schema")]
#[diagnostic(code(prax::schema::syntax_error))]
SyntaxError {
#[source_code]
src: String,
#[label("error here")]
span: miette::SourceSpan,
message: String,
},
#[error("invalid model `{name}`: {message}")]
#[diagnostic(code(prax::schema::invalid_model))]
InvalidModel { name: String, message: String },
#[error("invalid field `{model}.{field}`: {message}")]
#[diagnostic(code(prax::schema::invalid_field))]
InvalidField {
model: String,
field: String,
message: String,
},
#[error("invalid relation `{model}.{field}`: {message}")]
#[diagnostic(code(prax::schema::invalid_relation))]
InvalidRelation {
model: String,
field: String,
message: String,
},
#[error("duplicate {kind} `{name}`")]
#[diagnostic(code(prax::schema::duplicate))]
Duplicate { kind: String, name: String },
#[error("unknown type `{type_name}` in `{model}.{field}`")]
#[diagnostic(code(prax::schema::unknown_type))]
UnknownType {
model: String,
field: String,
type_name: String,
},
#[error("invalid attribute `@{attribute}`: {message}")]
#[diagnostic(code(prax::schema::invalid_attribute))]
InvalidAttribute { attribute: String, message: String },
#[error("model `{model}` is missing required `@id` field")]
#[diagnostic(code(prax::schema::missing_id))]
MissingId { model: String },
#[error("configuration error: {message}")]
#[diagnostic(code(prax::schema::config_error))]
ConfigError { message: String },
#[error("failed to parse TOML")]
#[diagnostic(code(prax::schema::toml_error))]
TomlError {
#[source]
source: toml::de::Error,
},
#[error("schema validation failed with {count} error(s)")]
#[diagnostic(code(prax::schema::validation_failed))]
ValidationFailed {
count: usize,
#[related]
errors: Vec<SchemaError>,
},
#[error("field '{field}' of type Vector is missing required @dim attribute")]
#[diagnostic(code(prax::schema::missing_vector_dimension))]
MissingVectorDimension {
field: String,
},
#[error(
"invalid vector element type '{value}' (expected one of: float2, float4, float8, int1, int2, int4)"
)]
#[diagnostic(code(prax::schema::invalid_vector_type))]
InvalidVectorType {
value: String,
},
#[error("invalid vector metric '{value}' (expected one of: cosine, l2, inner)")]
#[diagnostic(code(prax::schema::invalid_vector_metric))]
InvalidVectorMetric {
value: String,
},
#[error("invalid vector index '{value}' (expected: hnsw)")]
#[diagnostic(code(prax::schema::invalid_vector_index))]
InvalidVectorIndex {
value: String,
},
#[error("parse error in source {}", .source.0)]
#[diagnostic(code(prax::schema::parse_in_file))]
ParseInFile {
source: crate::loader::SourceId,
#[source]
inner: Box<SchemaError>,
},
#[error("duplicate {kind} `{name}` declared in two files")]
#[diagnostic(code(prax::schema::duplicate_across_files))]
DuplicateAcrossFiles {
kind: DuplicateKind,
name: String,
first: crate::loader::SourceLoc,
second: crate::loader::SourceLoc,
},
#[error("multiple datasource blocks declared (exactly one allowed across all files)")]
#[diagnostic(code(prax::schema::multiple_datasource))]
MultipleDatasource {
first: crate::loader::SourceLoc,
second: crate::loader::SourceLoc,
},
#[error("schema directory `{}` contains no .prax files", .path.display())]
#[diagnostic(code(prax::schema::empty_directory))]
EmptySchemaDirectory { path: std::path::PathBuf },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DuplicateKind {
Model,
Enum,
Type,
View,
ServerGroup,
Policy,
Generator,
RawSql,
}
impl std::fmt::Display for DuplicateKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
DuplicateKind::Model => "model",
DuplicateKind::Enum => "enum",
DuplicateKind::Type => "type",
DuplicateKind::View => "view",
DuplicateKind::ServerGroup => "serverGroup",
DuplicateKind::Policy => "policy",
DuplicateKind::Generator => "generator",
DuplicateKind::RawSql => "rawSql",
};
f.write_str(s)
}
}
impl SchemaError {
pub fn syntax(
src: impl Into<String>,
offset: usize,
len: usize,
message: impl Into<String>,
) -> Self {
Self::SyntaxError {
src: src.into(),
span: (offset, len).into(),
message: message.into(),
}
}
pub fn invalid_model(name: impl Into<String>, message: impl Into<String>) -> Self {
Self::InvalidModel {
name: name.into(),
message: message.into(),
}
}
pub fn invalid_field(
model: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::InvalidField {
model: model.into(),
field: field.into(),
message: message.into(),
}
}
pub fn invalid_relation(
model: impl Into<String>,
field: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::InvalidRelation {
model: model.into(),
field: field.into(),
message: message.into(),
}
}
pub fn duplicate(kind: impl Into<String>, name: impl Into<String>) -> Self {
Self::Duplicate {
kind: kind.into(),
name: name.into(),
}
}
pub fn unknown_type(
model: impl Into<String>,
field: impl Into<String>,
type_name: impl Into<String>,
) -> Self {
Self::UnknownType {
model: model.into(),
field: field.into(),
type_name: type_name.into(),
}
}
}
#[cfg(test)]
#[allow(unused_assignments)]
mod tests {
use super::*;
#[test]
#[allow(clippy::unnecessary_literal_unwrap)]
fn test_schema_result_type() {
let ok_result: SchemaResult<i32> = Ok(42);
assert!(ok_result.is_ok());
assert_eq!(ok_result.unwrap(), 42);
let err_result: SchemaResult<i32> = Err(SchemaError::ConfigError {
message: "test".to_string(),
});
assert!(err_result.is_err());
}
#[test]
fn test_syntax_error() {
let err = SchemaError::syntax("model User { }", 6, 4, "unexpected token");
match err {
SchemaError::SyntaxError { src, span, message } => {
assert_eq!(src, "model User { }");
assert_eq!(span.offset(), 6);
assert_eq!(span.len(), 4);
assert_eq!(message, "unexpected token");
}
_ => panic!("Expected SyntaxError"),
}
}
#[test]
fn test_invalid_model_error() {
let err = SchemaError::invalid_model("User", "missing id field");
match err {
SchemaError::InvalidModel { name, message } => {
assert_eq!(name, "User");
assert_eq!(message, "missing id field");
}
_ => panic!("Expected InvalidModel"),
}
}
#[test]
fn test_invalid_field_error() {
let err = SchemaError::invalid_field("User", "email", "invalid type");
match err {
SchemaError::InvalidField {
model,
field,
message,
} => {
assert_eq!(model, "User");
assert_eq!(field, "email");
assert_eq!(message, "invalid type");
}
_ => panic!("Expected InvalidField"),
}
}
#[test]
fn test_invalid_relation_error() {
let err = SchemaError::invalid_relation("Post", "author", "missing foreign key");
match err {
SchemaError::InvalidRelation {
model,
field,
message,
} => {
assert_eq!(model, "Post");
assert_eq!(field, "author");
assert_eq!(message, "missing foreign key");
}
_ => panic!("Expected InvalidRelation"),
}
}
#[test]
fn test_duplicate_error() {
let err = SchemaError::duplicate("model", "User");
match err {
SchemaError::Duplicate { kind, name } => {
assert_eq!(kind, "model");
assert_eq!(name, "User");
}
_ => panic!("Expected Duplicate"),
}
}
#[test]
fn test_unknown_type_error() {
let err = SchemaError::unknown_type("Post", "category", "Category");
match err {
SchemaError::UnknownType {
model,
field,
type_name,
} => {
assert_eq!(model, "Post");
assert_eq!(field, "category");
assert_eq!(type_name, "Category");
}
_ => panic!("Expected UnknownType"),
}
}
#[test]
fn test_io_error_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = SchemaError::IoError {
path: "schema.prax".to_string(),
source: io_err,
};
let display = format!("{}", err);
assert!(display.contains("schema.prax"));
}
#[test]
fn test_syntax_error_display() {
let err = SchemaError::syntax("model", 0, 5, "unexpected");
let display = format!("{}", err);
assert!(display.contains("syntax error"));
}
#[test]
fn test_invalid_model_display() {
let err = SchemaError::invalid_model("User", "test message");
let display = format!("{}", err);
assert!(display.contains("User"));
assert!(display.contains("test message"));
}
#[test]
fn test_invalid_field_display() {
let err = SchemaError::invalid_field("User", "email", "test");
let display = format!("{}", err);
assert!(display.contains("User.email"));
}
#[test]
fn test_invalid_relation_display() {
let err = SchemaError::invalid_relation("Post", "author", "test");
let display = format!("{}", err);
assert!(display.contains("Post.author"));
}
#[test]
fn test_duplicate_display() {
let err = SchemaError::duplicate("model", "User");
let display = format!("{}", err);
assert!(display.contains("duplicate"));
assert!(display.contains("model"));
assert!(display.contains("User"));
}
#[test]
fn test_unknown_type_display() {
let err = SchemaError::unknown_type("Post", "author", "UserType");
let display = format!("{}", err);
assert!(display.contains("UserType"));
assert!(display.contains("Post.author"));
}
#[test]
fn test_missing_id_display() {
let err = SchemaError::MissingId {
model: "User".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("User"));
assert!(display.contains("@id"));
}
#[test]
fn test_config_error_display() {
let err = SchemaError::ConfigError {
message: "invalid URL".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("invalid URL"));
}
#[test]
fn test_validation_failed_display() {
let err = SchemaError::ValidationFailed {
count: 3,
errors: vec![],
};
let display = format!("{}", err);
assert!(display.contains("3"));
}
#[test]
fn test_error_debug() {
let err = SchemaError::invalid_model("User", "test");
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidModel"));
assert!(debug.contains("User"));
}
#[test]
fn test_syntax_from_strings() {
let src = String::from("content");
let msg = String::from("message");
let err = SchemaError::syntax(src, 0, 7, msg);
if let SchemaError::SyntaxError { src, message, .. } = err {
assert_eq!(src, "content");
assert_eq!(message, "message");
} else {
panic!("Expected SyntaxError");
}
}
#[test]
fn test_invalid_model_from_strings() {
let name = String::from("Model");
let msg = String::from("error");
let err = SchemaError::invalid_model(name, msg);
if let SchemaError::InvalidModel { name, message } = err {
assert_eq!(name, "Model");
assert_eq!(message, "error");
} else {
panic!("Expected InvalidModel");
}
}
#[test]
fn test_invalid_field_from_strings() {
let model = String::from("User");
let field = String::from("email");
let msg = String::from("error");
let err = SchemaError::invalid_field(model, field, msg);
if let SchemaError::InvalidField {
model,
field,
message,
} = err
{
assert_eq!(model, "User");
assert_eq!(field, "email");
assert_eq!(message, "error");
} else {
panic!("Expected InvalidField");
}
}
#[test]
fn duplicate_across_files_displays_name_and_kind() {
use crate::ast::Span;
use crate::loader::{SourceId, SourceLoc};
let err = SchemaError::DuplicateAcrossFiles {
kind: DuplicateKind::Model,
name: "User".to_string(),
first: SourceLoc::new(SourceId(0), Span::new(0, 10)),
second: SourceLoc::new(SourceId(1), Span::new(0, 10)),
};
let msg = format!("{err}");
assert!(msg.contains("duplicate model"), "got: {msg}");
assert!(msg.contains("User"), "got: {msg}");
}
#[test]
fn empty_directory_displays_path() {
let err = SchemaError::EmptySchemaDirectory {
path: std::path::PathBuf::from("/tmp/empty"),
};
assert!(format!("{err}").contains("/tmp/empty"));
}
}