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}
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 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 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 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 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 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 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 #[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 #[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 #[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 #[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}