Skip to main content

prax_schema/
error.rs

1//! Error types for schema parsing and validation.
2
3// These warnings are false positives - the fields are used by derive macros
4#![allow(unused_assignments)]
5
6use miette::Diagnostic;
7use thiserror::Error;
8
9/// Result type for schema operations.
10pub type SchemaResult<T> = Result<T, SchemaError>;
11
12/// Errors that can occur during schema parsing and validation.
13#[derive(Error, Debug, Diagnostic)]
14pub enum SchemaError {
15    /// Error reading a file.
16    #[error("failed to read file: {path}")]
17    #[diagnostic(code(prax::schema::io_error))]
18    IoError {
19        path: String,
20        #[source]
21        source: std::io::Error,
22    },
23
24    /// Syntax error in the schema file.
25    #[error("syntax error in schema")]
26    #[diagnostic(code(prax::schema::syntax_error))]
27    SyntaxError {
28        #[source_code]
29        src: String,
30        #[label("error here")]
31        span: miette::SourceSpan,
32        message: String,
33    },
34
35    /// Invalid model definition.
36    #[error("invalid model `{name}`: {message}")]
37    #[diagnostic(code(prax::schema::invalid_model))]
38    InvalidModel { name: String, message: String },
39
40    /// Invalid field definition.
41    #[error("invalid field `{model}.{field}`: {message}")]
42    #[diagnostic(code(prax::schema::invalid_field))]
43    InvalidField {
44        model: String,
45        field: String,
46        message: String,
47    },
48
49    /// Invalid relation definition.
50    #[error("invalid relation `{model}.{field}`: {message}")]
51    #[diagnostic(code(prax::schema::invalid_relation))]
52    InvalidRelation {
53        model: String,
54        field: String,
55        message: String,
56    },
57
58    /// Duplicate definition.
59    #[error("duplicate {kind} `{name}`")]
60    #[diagnostic(code(prax::schema::duplicate))]
61    Duplicate { kind: String, name: String },
62
63    /// Unknown type reference.
64    #[error("unknown type `{type_name}` in `{model}.{field}`")]
65    #[diagnostic(code(prax::schema::unknown_type))]
66    UnknownType {
67        model: String,
68        field: String,
69        type_name: String,
70    },
71
72    /// Invalid attribute.
73    #[error("invalid attribute `@{attribute}`: {message}")]
74    #[diagnostic(code(prax::schema::invalid_attribute))]
75    InvalidAttribute { attribute: String, message: String },
76
77    /// Missing required attribute.
78    #[error("model `{model}` is missing required `@id` field")]
79    #[diagnostic(code(prax::schema::missing_id))]
80    MissingId { model: String },
81
82    /// Configuration error.
83    #[error("configuration error: {message}")]
84    #[diagnostic(code(prax::schema::config_error))]
85    ConfigError { message: String },
86
87    /// TOML parsing error.
88    #[error("failed to parse TOML")]
89    #[diagnostic(code(prax::schema::toml_error))]
90    TomlError {
91        #[source]
92        source: toml::de::Error,
93    },
94
95    /// Validation error with multiple issues.
96    #[error("schema validation failed with {count} error(s)")]
97    #[diagnostic(code(prax::schema::validation_failed))]
98    ValidationFailed {
99        count: usize,
100        #[related]
101        errors: Vec<SchemaError>,
102    },
103
104    /// A Vector field is missing a required @dim(N) attribute.
105    #[error("field '{field}' of type Vector is missing required @dim attribute")]
106    #[diagnostic(code(prax::schema::missing_vector_dimension))]
107    MissingVectorDimension {
108        /// Field name.
109        field: String,
110    },
111
112    /// A Vector field has an invalid @vectorType value.
113    #[error(
114        "invalid vector element type '{value}' (expected one of: float2, float4, float8, int1, int2, int4)"
115    )]
116    #[diagnostic(code(prax::schema::invalid_vector_type))]
117    InvalidVectorType {
118        /// Supplied type value.
119        value: String,
120    },
121
122    /// A Vector field has an invalid @metric value.
123    #[error("invalid vector metric '{value}' (expected one of: cosine, l2, inner)")]
124    #[diagnostic(code(prax::schema::invalid_vector_metric))]
125    InvalidVectorMetric {
126        /// Supplied metric value.
127        value: String,
128    },
129
130    /// A Vector field has an invalid @index value.
131    #[error("invalid vector index '{value}' (expected: hnsw)")]
132    #[diagnostic(code(prax::schema::invalid_vector_index))]
133    InvalidVectorIndex {
134        /// Supplied index value.
135        value: String,
136    },
137
138    /// Parse failure in a specific file within a multi-file schema directory.
139    #[error("parse error in source {}", .source.0)]
140    #[diagnostic(code(prax::schema::parse_in_file))]
141    ParseInFile {
142        source: crate::loader::SourceId,
143        #[source]
144        inner: Box<SchemaError>,
145    },
146
147    /// Same-named item declared in two source files.
148    #[error("duplicate {kind} `{name}` declared in two files")]
149    #[diagnostic(code(prax::schema::duplicate_across_files))]
150    DuplicateAcrossFiles {
151        kind: DuplicateKind,
152        name: String,
153        first: crate::loader::SourceLoc,
154        second: crate::loader::SourceLoc,
155    },
156
157    /// More than one `datasource` block across source files.
158    #[error("multiple datasource blocks declared (exactly one allowed across all files)")]
159    #[diagnostic(code(prax::schema::multiple_datasource))]
160    MultipleDatasource {
161        first: crate::loader::SourceLoc,
162        second: crate::loader::SourceLoc,
163    },
164
165    /// Schema directory contained no `*.prax` files.
166    #[error("schema directory `{}` contains no .prax files", .path.display())]
167    #[diagnostic(code(prax::schema::empty_directory))]
168    EmptySchemaDirectory { path: std::path::PathBuf },
169}
170
171/// The kind of top-level item involved in a cross-file duplicate-name error.
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum DuplicateKind {
174    Model,
175    Enum,
176    Type,
177    View,
178    ServerGroup,
179    Policy,
180    Generator,
181    RawSql,
182}
183
184impl std::fmt::Display for DuplicateKind {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        let s = match self {
187            DuplicateKind::Model => "model",
188            DuplicateKind::Enum => "enum",
189            DuplicateKind::Type => "type",
190            DuplicateKind::View => "view",
191            DuplicateKind::ServerGroup => "serverGroup",
192            DuplicateKind::Policy => "policy",
193            DuplicateKind::Generator => "generator",
194            DuplicateKind::RawSql => "rawSql",
195        };
196        f.write_str(s)
197    }
198}
199
200impl SchemaError {
201    /// Create a syntax error with source location.
202    pub fn syntax(
203        src: impl Into<String>,
204        offset: usize,
205        len: usize,
206        message: impl Into<String>,
207    ) -> Self {
208        Self::SyntaxError {
209            src: src.into(),
210            span: (offset, len).into(),
211            message: message.into(),
212        }
213    }
214
215    /// Create an invalid model error.
216    pub fn invalid_model(name: impl Into<String>, message: impl Into<String>) -> Self {
217        Self::InvalidModel {
218            name: name.into(),
219            message: message.into(),
220        }
221    }
222
223    /// Create an invalid field error.
224    pub fn invalid_field(
225        model: impl Into<String>,
226        field: impl Into<String>,
227        message: impl Into<String>,
228    ) -> Self {
229        Self::InvalidField {
230            model: model.into(),
231            field: field.into(),
232            message: message.into(),
233        }
234    }
235
236    /// Create an invalid relation error.
237    pub fn invalid_relation(
238        model: impl Into<String>,
239        field: impl Into<String>,
240        message: impl Into<String>,
241    ) -> Self {
242        Self::InvalidRelation {
243            model: model.into(),
244            field: field.into(),
245            message: message.into(),
246        }
247    }
248
249    /// Create a duplicate definition error.
250    pub fn duplicate(kind: impl Into<String>, name: impl Into<String>) -> Self {
251        Self::Duplicate {
252            kind: kind.into(),
253            name: name.into(),
254        }
255    }
256
257    /// Create an unknown type error.
258    pub fn unknown_type(
259        model: impl Into<String>,
260        field: impl Into<String>,
261        type_name: impl Into<String>,
262    ) -> Self {
263        Self::UnknownType {
264            model: model.into(),
265            field: field.into(),
266            type_name: type_name.into(),
267        }
268    }
269}
270
271#[cfg(test)]
272#[allow(unused_assignments)]
273mod tests {
274    use super::*;
275
276    #[test]
277    #[allow(clippy::unnecessary_literal_unwrap)]
278    fn test_schema_result_type() {
279        let ok_result: SchemaResult<i32> = Ok(42);
280        assert!(ok_result.is_ok());
281        assert_eq!(ok_result.unwrap(), 42);
282
283        let err_result: SchemaResult<i32> = Err(SchemaError::ConfigError {
284            message: "test".to_string(),
285        });
286        assert!(err_result.is_err());
287    }
288
289    // ==================== Error Constructor Tests ====================
290
291    #[test]
292    fn test_syntax_error() {
293        let err = SchemaError::syntax("model User { }", 6, 4, "unexpected token");
294
295        match err {
296            SchemaError::SyntaxError { src, span, message } => {
297                assert_eq!(src, "model User { }");
298                assert_eq!(span.offset(), 6);
299                assert_eq!(span.len(), 4);
300                assert_eq!(message, "unexpected token");
301            }
302            _ => panic!("Expected SyntaxError"),
303        }
304    }
305
306    #[test]
307    fn test_invalid_model_error() {
308        let err = SchemaError::invalid_model("User", "missing id field");
309
310        match err {
311            SchemaError::InvalidModel { name, message } => {
312                assert_eq!(name, "User");
313                assert_eq!(message, "missing id field");
314            }
315            _ => panic!("Expected InvalidModel"),
316        }
317    }
318
319    #[test]
320    fn test_invalid_field_error() {
321        let err = SchemaError::invalid_field("User", "email", "invalid type");
322
323        match err {
324            SchemaError::InvalidField {
325                model,
326                field,
327                message,
328            } => {
329                assert_eq!(model, "User");
330                assert_eq!(field, "email");
331                assert_eq!(message, "invalid type");
332            }
333            _ => panic!("Expected InvalidField"),
334        }
335    }
336
337    #[test]
338    fn test_invalid_relation_error() {
339        let err = SchemaError::invalid_relation("Post", "author", "missing foreign key");
340
341        match err {
342            SchemaError::InvalidRelation {
343                model,
344                field,
345                message,
346            } => {
347                assert_eq!(model, "Post");
348                assert_eq!(field, "author");
349                assert_eq!(message, "missing foreign key");
350            }
351            _ => panic!("Expected InvalidRelation"),
352        }
353    }
354
355    #[test]
356    fn test_duplicate_error() {
357        let err = SchemaError::duplicate("model", "User");
358
359        match err {
360            SchemaError::Duplicate { kind, name } => {
361                assert_eq!(kind, "model");
362                assert_eq!(name, "User");
363            }
364            _ => panic!("Expected Duplicate"),
365        }
366    }
367
368    #[test]
369    fn test_unknown_type_error() {
370        let err = SchemaError::unknown_type("Post", "category", "Category");
371
372        match err {
373            SchemaError::UnknownType {
374                model,
375                field,
376                type_name,
377            } => {
378                assert_eq!(model, "Post");
379                assert_eq!(field, "category");
380                assert_eq!(type_name, "Category");
381            }
382            _ => panic!("Expected UnknownType"),
383        }
384    }
385
386    // ==================== Error Display Tests ====================
387
388    #[test]
389    fn test_io_error_display() {
390        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
391        let err = SchemaError::IoError {
392            path: "schema.prax".to_string(),
393            source: io_err,
394        };
395
396        let display = format!("{}", err);
397        assert!(display.contains("schema.prax"));
398    }
399
400    #[test]
401    fn test_syntax_error_display() {
402        let err = SchemaError::syntax("model", 0, 5, "unexpected");
403        let display = format!("{}", err);
404        assert!(display.contains("syntax error"));
405    }
406
407    #[test]
408    fn test_invalid_model_display() {
409        let err = SchemaError::invalid_model("User", "test message");
410        let display = format!("{}", err);
411        assert!(display.contains("User"));
412        assert!(display.contains("test message"));
413    }
414
415    #[test]
416    fn test_invalid_field_display() {
417        let err = SchemaError::invalid_field("User", "email", "test");
418        let display = format!("{}", err);
419        assert!(display.contains("User.email"));
420    }
421
422    #[test]
423    fn test_invalid_relation_display() {
424        let err = SchemaError::invalid_relation("Post", "author", "test");
425        let display = format!("{}", err);
426        assert!(display.contains("Post.author"));
427    }
428
429    #[test]
430    fn test_duplicate_display() {
431        let err = SchemaError::duplicate("model", "User");
432        let display = format!("{}", err);
433        assert!(display.contains("duplicate"));
434        assert!(display.contains("model"));
435        assert!(display.contains("User"));
436    }
437
438    #[test]
439    fn test_unknown_type_display() {
440        let err = SchemaError::unknown_type("Post", "author", "UserType");
441        let display = format!("{}", err);
442        assert!(display.contains("UserType"));
443        assert!(display.contains("Post.author"));
444    }
445
446    #[test]
447    fn test_missing_id_display() {
448        let err = SchemaError::MissingId {
449            model: "User".to_string(),
450        };
451        let display = format!("{}", err);
452        assert!(display.contains("User"));
453        assert!(display.contains("@id"));
454    }
455
456    #[test]
457    fn test_config_error_display() {
458        let err = SchemaError::ConfigError {
459            message: "invalid URL".to_string(),
460        };
461        let display = format!("{}", err);
462        assert!(display.contains("invalid URL"));
463    }
464
465    #[test]
466    fn test_validation_failed_display() {
467        let err = SchemaError::ValidationFailed {
468            count: 3,
469            errors: vec![],
470        };
471        let display = format!("{}", err);
472        assert!(display.contains("3"));
473    }
474
475    // ==================== Error Debug Tests ====================
476
477    #[test]
478    fn test_error_debug() {
479        let err = SchemaError::invalid_model("User", "test");
480        let debug = format!("{:?}", err);
481        assert!(debug.contains("InvalidModel"));
482        assert!(debug.contains("User"));
483    }
484
485    // ==================== Error From Constructors Tests ====================
486
487    #[test]
488    fn test_syntax_from_strings() {
489        let src = String::from("content");
490        let msg = String::from("message");
491        let err = SchemaError::syntax(src, 0, 7, msg);
492
493        if let SchemaError::SyntaxError { src, message, .. } = err {
494            assert_eq!(src, "content");
495            assert_eq!(message, "message");
496        } else {
497            panic!("Expected SyntaxError");
498        }
499    }
500
501    #[test]
502    fn test_invalid_model_from_strings() {
503        let name = String::from("Model");
504        let msg = String::from("error");
505        let err = SchemaError::invalid_model(name, msg);
506
507        if let SchemaError::InvalidModel { name, message } = err {
508            assert_eq!(name, "Model");
509            assert_eq!(message, "error");
510        } else {
511            panic!("Expected InvalidModel");
512        }
513    }
514
515    #[test]
516    fn test_invalid_field_from_strings() {
517        let model = String::from("User");
518        let field = String::from("email");
519        let msg = String::from("error");
520        let err = SchemaError::invalid_field(model, field, msg);
521
522        if let SchemaError::InvalidField {
523            model,
524            field,
525            message,
526        } = err
527        {
528            assert_eq!(model, "User");
529            assert_eq!(field, "email");
530            assert_eq!(message, "error");
531        } else {
532            panic!("Expected InvalidField");
533        }
534    }
535
536    #[test]
537    fn duplicate_across_files_displays_name_and_kind() {
538        use crate::ast::Span;
539        use crate::loader::{SourceId, SourceLoc};
540
541        let err = SchemaError::DuplicateAcrossFiles {
542            kind: DuplicateKind::Model,
543            name: "User".to_string(),
544            first: SourceLoc::new(SourceId(0), Span::new(0, 10)),
545            second: SourceLoc::new(SourceId(1), Span::new(0, 10)),
546        };
547        let msg = format!("{err}");
548        assert!(msg.contains("duplicate model"), "got: {msg}");
549        assert!(msg.contains("User"), "got: {msg}");
550    }
551
552    #[test]
553    fn empty_directory_displays_path() {
554        let err = SchemaError::EmptySchemaDirectory {
555            path: std::path::PathBuf::from("/tmp/empty"),
556        };
557        assert!(format!("{err}").contains("/tmp/empty"));
558    }
559}