1#![allow(unused_assignments)]
5
6use miette::Diagnostic;
7use thiserror::Error;
8
9pub type SchemaResult<T> = Result<T, SchemaError>;
11
12#[derive(Error, Debug, Diagnostic)]
14pub enum SchemaError {
15 #[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 #[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 #[error("invalid model `{name}`: {message}")]
37 #[diagnostic(code(prax::schema::invalid_model))]
38 InvalidModel { name: String, message: String },
39
40 #[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 #[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 #[error("duplicate {kind} `{name}`")]
60 #[diagnostic(code(prax::schema::duplicate))]
61 Duplicate { kind: String, name: String },
62
63 #[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 #[error("invalid attribute `@{attribute}`: {message}")]
74 #[diagnostic(code(prax::schema::invalid_attribute))]
75 InvalidAttribute { attribute: String, message: String },
76
77 #[error("model `{model}` is missing required `@id` field")]
79 #[diagnostic(code(prax::schema::missing_id))]
80 MissingId { model: String },
81
82 #[error("configuration error: {message}")]
84 #[diagnostic(code(prax::schema::config_error))]
85 ConfigError { message: String },
86
87 #[error("failed to parse TOML")]
89 #[diagnostic(code(prax::schema::toml_error))]
90 TomlError {
91 #[source]
92 source: toml::de::Error,
93 },
94
95 #[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 #[error("field '{field}' of type Vector is missing required @dim attribute")]
106 #[diagnostic(code(prax::schema::missing_vector_dimension))]
107 MissingVectorDimension {
108 field: String,
110 },
111
112 #[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 value: String,
120 },
121
122 #[error("invalid vector metric '{value}' (expected one of: cosine, l2, inner)")]
124 #[diagnostic(code(prax::schema::invalid_vector_metric))]
125 InvalidVectorMetric {
126 value: String,
128 },
129
130 #[error("invalid vector index '{value}' (expected: hnsw)")]
132 #[diagnostic(code(prax::schema::invalid_vector_index))]
133 InvalidVectorIndex {
134 value: String,
136 },
137}
138
139impl SchemaError {
140 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 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 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 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 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 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 #[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 #[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 #[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 #[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}