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