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
139impl SchemaError {
140    /// Create a syntax error with source location.
141    pub fn syntax(
142        src: impl Into<String>,
143        offset: usize,
144        len: usize,
145        message: impl Into<String>,
146    ) -> Self {
147        Self::SyntaxError {
148            src: src.into(),
149            span: (offset, len).into(),
150            message: message.into(),
151        }
152    }
153
154    /// Create an invalid model error.
155    pub fn invalid_model(name: impl Into<String>, message: impl Into<String>) -> Self {
156        Self::InvalidModel {
157            name: name.into(),
158            message: message.into(),
159        }
160    }
161
162    /// Create an invalid field error.
163    pub fn invalid_field(
164        model: impl Into<String>,
165        field: impl Into<String>,
166        message: impl Into<String>,
167    ) -> Self {
168        Self::InvalidField {
169            model: model.into(),
170            field: field.into(),
171            message: message.into(),
172        }
173    }
174
175    /// Create an invalid relation error.
176    pub fn invalid_relation(
177        model: impl Into<String>,
178        field: impl Into<String>,
179        message: impl Into<String>,
180    ) -> Self {
181        Self::InvalidRelation {
182            model: model.into(),
183            field: field.into(),
184            message: message.into(),
185        }
186    }
187
188    /// Create a duplicate definition error.
189    pub fn duplicate(kind: impl Into<String>, name: impl Into<String>) -> Self {
190        Self::Duplicate {
191            kind: kind.into(),
192            name: name.into(),
193        }
194    }
195
196    /// Create an unknown type error.
197    pub fn unknown_type(
198        model: impl Into<String>,
199        field: impl Into<String>,
200        type_name: impl Into<String>,
201    ) -> Self {
202        Self::UnknownType {
203            model: model.into(),
204            field: field.into(),
205            type_name: type_name.into(),
206        }
207    }
208}
209
210#[cfg(test)]
211#[allow(unused_assignments)]
212mod tests {
213    use super::*;
214
215    #[test]
216    #[allow(clippy::unnecessary_literal_unwrap)]
217    fn test_schema_result_type() {
218        let ok_result: SchemaResult<i32> = Ok(42);
219        assert!(ok_result.is_ok());
220        assert_eq!(ok_result.unwrap(), 42);
221
222        let err_result: SchemaResult<i32> = Err(SchemaError::ConfigError {
223            message: "test".to_string(),
224        });
225        assert!(err_result.is_err());
226    }
227
228    // ==================== Error Constructor Tests ====================
229
230    #[test]
231    fn test_syntax_error() {
232        let err = SchemaError::syntax("model User { }", 6, 4, "unexpected token");
233
234        match err {
235            SchemaError::SyntaxError { src, span, message } => {
236                assert_eq!(src, "model User { }");
237                assert_eq!(span.offset(), 6);
238                assert_eq!(span.len(), 4);
239                assert_eq!(message, "unexpected token");
240            }
241            _ => panic!("Expected SyntaxError"),
242        }
243    }
244
245    #[test]
246    fn test_invalid_model_error() {
247        let err = SchemaError::invalid_model("User", "missing id field");
248
249        match err {
250            SchemaError::InvalidModel { name, message } => {
251                assert_eq!(name, "User");
252                assert_eq!(message, "missing id field");
253            }
254            _ => panic!("Expected InvalidModel"),
255        }
256    }
257
258    #[test]
259    fn test_invalid_field_error() {
260        let err = SchemaError::invalid_field("User", "email", "invalid type");
261
262        match err {
263            SchemaError::InvalidField {
264                model,
265                field,
266                message,
267            } => {
268                assert_eq!(model, "User");
269                assert_eq!(field, "email");
270                assert_eq!(message, "invalid type");
271            }
272            _ => panic!("Expected InvalidField"),
273        }
274    }
275
276    #[test]
277    fn test_invalid_relation_error() {
278        let err = SchemaError::invalid_relation("Post", "author", "missing foreign key");
279
280        match err {
281            SchemaError::InvalidRelation {
282                model,
283                field,
284                message,
285            } => {
286                assert_eq!(model, "Post");
287                assert_eq!(field, "author");
288                assert_eq!(message, "missing foreign key");
289            }
290            _ => panic!("Expected InvalidRelation"),
291        }
292    }
293
294    #[test]
295    fn test_duplicate_error() {
296        let err = SchemaError::duplicate("model", "User");
297
298        match err {
299            SchemaError::Duplicate { kind, name } => {
300                assert_eq!(kind, "model");
301                assert_eq!(name, "User");
302            }
303            _ => panic!("Expected Duplicate"),
304        }
305    }
306
307    #[test]
308    fn test_unknown_type_error() {
309        let err = SchemaError::unknown_type("Post", "category", "Category");
310
311        match err {
312            SchemaError::UnknownType {
313                model,
314                field,
315                type_name,
316            } => {
317                assert_eq!(model, "Post");
318                assert_eq!(field, "category");
319                assert_eq!(type_name, "Category");
320            }
321            _ => panic!("Expected UnknownType"),
322        }
323    }
324
325    // ==================== Error Display Tests ====================
326
327    #[test]
328    fn test_io_error_display() {
329        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
330        let err = SchemaError::IoError {
331            path: "schema.prax".to_string(),
332            source: io_err,
333        };
334
335        let display = format!("{}", err);
336        assert!(display.contains("schema.prax"));
337    }
338
339    #[test]
340    fn test_syntax_error_display() {
341        let err = SchemaError::syntax("model", 0, 5, "unexpected");
342        let display = format!("{}", err);
343        assert!(display.contains("syntax error"));
344    }
345
346    #[test]
347    fn test_invalid_model_display() {
348        let err = SchemaError::invalid_model("User", "test message");
349        let display = format!("{}", err);
350        assert!(display.contains("User"));
351        assert!(display.contains("test message"));
352    }
353
354    #[test]
355    fn test_invalid_field_display() {
356        let err = SchemaError::invalid_field("User", "email", "test");
357        let display = format!("{}", err);
358        assert!(display.contains("User.email"));
359    }
360
361    #[test]
362    fn test_invalid_relation_display() {
363        let err = SchemaError::invalid_relation("Post", "author", "test");
364        let display = format!("{}", err);
365        assert!(display.contains("Post.author"));
366    }
367
368    #[test]
369    fn test_duplicate_display() {
370        let err = SchemaError::duplicate("model", "User");
371        let display = format!("{}", err);
372        assert!(display.contains("duplicate"));
373        assert!(display.contains("model"));
374        assert!(display.contains("User"));
375    }
376
377    #[test]
378    fn test_unknown_type_display() {
379        let err = SchemaError::unknown_type("Post", "author", "UserType");
380        let display = format!("{}", err);
381        assert!(display.contains("UserType"));
382        assert!(display.contains("Post.author"));
383    }
384
385    #[test]
386    fn test_missing_id_display() {
387        let err = SchemaError::MissingId {
388            model: "User".to_string(),
389        };
390        let display = format!("{}", err);
391        assert!(display.contains("User"));
392        assert!(display.contains("@id"));
393    }
394
395    #[test]
396    fn test_config_error_display() {
397        let err = SchemaError::ConfigError {
398            message: "invalid URL".to_string(),
399        };
400        let display = format!("{}", err);
401        assert!(display.contains("invalid URL"));
402    }
403
404    #[test]
405    fn test_validation_failed_display() {
406        let err = SchemaError::ValidationFailed {
407            count: 3,
408            errors: vec![],
409        };
410        let display = format!("{}", err);
411        assert!(display.contains("3"));
412    }
413
414    // ==================== Error Debug Tests ====================
415
416    #[test]
417    fn test_error_debug() {
418        let err = SchemaError::invalid_model("User", "test");
419        let debug = format!("{:?}", err);
420        assert!(debug.contains("InvalidModel"));
421        assert!(debug.contains("User"));
422    }
423
424    // ==================== Error From Constructors Tests ====================
425
426    #[test]
427    fn test_syntax_from_strings() {
428        let src = String::from("content");
429        let msg = String::from("message");
430        let err = SchemaError::syntax(src, 0, 7, msg);
431
432        if let SchemaError::SyntaxError { src, message, .. } = err {
433            assert_eq!(src, "content");
434            assert_eq!(message, "message");
435        } else {
436            panic!("Expected SyntaxError");
437        }
438    }
439
440    #[test]
441    fn test_invalid_model_from_strings() {
442        let name = String::from("Model");
443        let msg = String::from("error");
444        let err = SchemaError::invalid_model(name, msg);
445
446        if let SchemaError::InvalidModel { name, message } = err {
447            assert_eq!(name, "Model");
448            assert_eq!(message, "error");
449        } else {
450            panic!("Expected InvalidModel");
451        }
452    }
453
454    #[test]
455    fn test_invalid_field_from_strings() {
456        let model = String::from("User");
457        let field = String::from("email");
458        let msg = String::from("error");
459        let err = SchemaError::invalid_field(model, field, msg);
460
461        if let SchemaError::InvalidField {
462            model,
463            field,
464            message,
465        } = err
466        {
467            assert_eq!(model, "User");
468            assert_eq!(field, "email");
469            assert_eq!(message, "error");
470        } else {
471            panic!("Expected InvalidField");
472        }
473    }
474}