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 #[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 #[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 #[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 #[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#[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 Procedure,
183}
184
185impl std::fmt::Display for DuplicateKind {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 let s = match self {
188 DuplicateKind::Model => "model",
189 DuplicateKind::Enum => "enum",
190 DuplicateKind::Type => "type",
191 DuplicateKind::View => "view",
192 DuplicateKind::ServerGroup => "serverGroup",
193 DuplicateKind::Policy => "policy",
194 DuplicateKind::Generator => "generator",
195 DuplicateKind::RawSql => "rawSql",
196 DuplicateKind::Procedure => "procedure",
197 };
198 f.write_str(s)
199 }
200}
201
202impl SchemaError {
203 pub fn syntax(
205 src: impl Into<String>,
206 offset: usize,
207 len: usize,
208 message: impl Into<String>,
209 ) -> Self {
210 Self::SyntaxError {
211 src: src.into(),
212 span: (offset, len).into(),
213 message: message.into(),
214 }
215 }
216
217 pub fn invalid_model(name: impl Into<String>, message: impl Into<String>) -> Self {
219 Self::InvalidModel {
220 name: name.into(),
221 message: message.into(),
222 }
223 }
224
225 pub fn invalid_field(
227 model: impl Into<String>,
228 field: impl Into<String>,
229 message: impl Into<String>,
230 ) -> Self {
231 Self::InvalidField {
232 model: model.into(),
233 field: field.into(),
234 message: message.into(),
235 }
236 }
237
238 pub fn invalid_relation(
240 model: impl Into<String>,
241 field: impl Into<String>,
242 message: impl Into<String>,
243 ) -> Self {
244 Self::InvalidRelation {
245 model: model.into(),
246 field: field.into(),
247 message: message.into(),
248 }
249 }
250
251 pub fn duplicate(kind: impl Into<String>, name: impl Into<String>) -> Self {
253 Self::Duplicate {
254 kind: kind.into(),
255 name: name.into(),
256 }
257 }
258
259 pub fn unknown_type(
261 model: impl Into<String>,
262 field: impl Into<String>,
263 type_name: impl Into<String>,
264 ) -> Self {
265 Self::UnknownType {
266 model: model.into(),
267 field: field.into(),
268 type_name: type_name.into(),
269 }
270 }
271}
272
273#[cfg(test)]
274#[allow(unused_assignments)]
275mod tests {
276 use super::*;
277
278 #[test]
279 #[allow(clippy::unnecessary_literal_unwrap)]
280 fn test_schema_result_type() {
281 let ok_result: SchemaResult<i32> = Ok(42);
282 assert!(ok_result.is_ok());
283 assert_eq!(ok_result.unwrap(), 42);
284
285 let err_result: SchemaResult<i32> = Err(SchemaError::ConfigError {
286 message: "test".to_string(),
287 });
288 assert!(err_result.is_err());
289 }
290
291 #[test]
294 fn test_syntax_error() {
295 let err = SchemaError::syntax("model User { }", 6, 4, "unexpected token");
296
297 match err {
298 SchemaError::SyntaxError { src, span, message } => {
299 assert_eq!(src, "model User { }");
300 assert_eq!(span.offset(), 6);
301 assert_eq!(span.len(), 4);
302 assert_eq!(message, "unexpected token");
303 }
304 _ => panic!("Expected SyntaxError"),
305 }
306 }
307
308 #[test]
309 fn test_invalid_model_error() {
310 let err = SchemaError::invalid_model("User", "missing id field");
311
312 match err {
313 SchemaError::InvalidModel { name, message } => {
314 assert_eq!(name, "User");
315 assert_eq!(message, "missing id field");
316 }
317 _ => panic!("Expected InvalidModel"),
318 }
319 }
320
321 #[test]
322 fn test_invalid_field_error() {
323 let err = SchemaError::invalid_field("User", "email", "invalid type");
324
325 match err {
326 SchemaError::InvalidField {
327 model,
328 field,
329 message,
330 } => {
331 assert_eq!(model, "User");
332 assert_eq!(field, "email");
333 assert_eq!(message, "invalid type");
334 }
335 _ => panic!("Expected InvalidField"),
336 }
337 }
338
339 #[test]
340 fn test_invalid_relation_error() {
341 let err = SchemaError::invalid_relation("Post", "author", "missing foreign key");
342
343 match err {
344 SchemaError::InvalidRelation {
345 model,
346 field,
347 message,
348 } => {
349 assert_eq!(model, "Post");
350 assert_eq!(field, "author");
351 assert_eq!(message, "missing foreign key");
352 }
353 _ => panic!("Expected InvalidRelation"),
354 }
355 }
356
357 #[test]
358 fn test_duplicate_error() {
359 let err = SchemaError::duplicate("model", "User");
360
361 match err {
362 SchemaError::Duplicate { kind, name } => {
363 assert_eq!(kind, "model");
364 assert_eq!(name, "User");
365 }
366 _ => panic!("Expected Duplicate"),
367 }
368 }
369
370 #[test]
371 fn test_unknown_type_error() {
372 let err = SchemaError::unknown_type("Post", "category", "Category");
373
374 match err {
375 SchemaError::UnknownType {
376 model,
377 field,
378 type_name,
379 } => {
380 assert_eq!(model, "Post");
381 assert_eq!(field, "category");
382 assert_eq!(type_name, "Category");
383 }
384 _ => panic!("Expected UnknownType"),
385 }
386 }
387
388 #[test]
391 fn test_io_error_display() {
392 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
393 let err = SchemaError::IoError {
394 path: "schema.prax".to_string(),
395 source: io_err,
396 };
397
398 let display = format!("{}", err);
399 assert!(display.contains("schema.prax"));
400 }
401
402 #[test]
403 fn test_syntax_error_display() {
404 let err = SchemaError::syntax("model", 0, 5, "unexpected");
405 let display = format!("{}", err);
406 assert!(display.contains("syntax error"));
407 }
408
409 #[test]
410 fn test_invalid_model_display() {
411 let err = SchemaError::invalid_model("User", "test message");
412 let display = format!("{}", err);
413 assert!(display.contains("User"));
414 assert!(display.contains("test message"));
415 }
416
417 #[test]
418 fn test_invalid_field_display() {
419 let err = SchemaError::invalid_field("User", "email", "test");
420 let display = format!("{}", err);
421 assert!(display.contains("User.email"));
422 }
423
424 #[test]
425 fn test_invalid_relation_display() {
426 let err = SchemaError::invalid_relation("Post", "author", "test");
427 let display = format!("{}", err);
428 assert!(display.contains("Post.author"));
429 }
430
431 #[test]
432 fn test_duplicate_display() {
433 let err = SchemaError::duplicate("model", "User");
434 let display = format!("{}", err);
435 assert!(display.contains("duplicate"));
436 assert!(display.contains("model"));
437 assert!(display.contains("User"));
438 }
439
440 #[test]
441 fn test_unknown_type_display() {
442 let err = SchemaError::unknown_type("Post", "author", "UserType");
443 let display = format!("{}", err);
444 assert!(display.contains("UserType"));
445 assert!(display.contains("Post.author"));
446 }
447
448 #[test]
449 fn test_missing_id_display() {
450 let err = SchemaError::MissingId {
451 model: "User".to_string(),
452 };
453 let display = format!("{}", err);
454 assert!(display.contains("User"));
455 assert!(display.contains("@id"));
456 }
457
458 #[test]
459 fn test_config_error_display() {
460 let err = SchemaError::ConfigError {
461 message: "invalid URL".to_string(),
462 };
463 let display = format!("{}", err);
464 assert!(display.contains("invalid URL"));
465 }
466
467 #[test]
468 fn test_validation_failed_display() {
469 let err = SchemaError::ValidationFailed {
470 count: 3,
471 errors: vec![],
472 };
473 let display = format!("{}", err);
474 assert!(display.contains("3"));
475 }
476
477 #[test]
480 fn test_error_debug() {
481 let err = SchemaError::invalid_model("User", "test");
482 let debug = format!("{:?}", err);
483 assert!(debug.contains("InvalidModel"));
484 assert!(debug.contains("User"));
485 }
486
487 #[test]
490 fn test_syntax_from_strings() {
491 let src = String::from("content");
492 let msg = String::from("message");
493 let err = SchemaError::syntax(src, 0, 7, msg);
494
495 if let SchemaError::SyntaxError { src, message, .. } = err {
496 assert_eq!(src, "content");
497 assert_eq!(message, "message");
498 } else {
499 panic!("Expected SyntaxError");
500 }
501 }
502
503 #[test]
504 fn test_invalid_model_from_strings() {
505 let name = String::from("Model");
506 let msg = String::from("error");
507 let err = SchemaError::invalid_model(name, msg);
508
509 if let SchemaError::InvalidModel { name, message } = err {
510 assert_eq!(name, "Model");
511 assert_eq!(message, "error");
512 } else {
513 panic!("Expected InvalidModel");
514 }
515 }
516
517 #[test]
518 fn test_invalid_field_from_strings() {
519 let model = String::from("User");
520 let field = String::from("email");
521 let msg = String::from("error");
522 let err = SchemaError::invalid_field(model, field, msg);
523
524 if let SchemaError::InvalidField {
525 model,
526 field,
527 message,
528 } = err
529 {
530 assert_eq!(model, "User");
531 assert_eq!(field, "email");
532 assert_eq!(message, "error");
533 } else {
534 panic!("Expected InvalidField");
535 }
536 }
537
538 #[test]
539 fn duplicate_across_files_displays_name_and_kind() {
540 use crate::ast::Span;
541 use crate::loader::{SourceId, SourceLoc};
542
543 let err = SchemaError::DuplicateAcrossFiles {
544 kind: DuplicateKind::Model,
545 name: "User".to_string(),
546 first: SourceLoc::new(SourceId(0), Span::new(0, 10)),
547 second: SourceLoc::new(SourceId(1), Span::new(0, 10)),
548 };
549 let msg = format!("{err}");
550 assert!(msg.contains("duplicate model"), "got: {msg}");
551 assert!(msg.contains("User"), "got: {msg}");
552 }
553
554 #[test]
555 fn empty_directory_displays_path() {
556 let err = SchemaError::EmptySchemaDirectory {
557 path: std::path::PathBuf::from("/tmp/empty"),
558 };
559 assert!(format!("{err}").contains("/tmp/empty"));
560 }
561}