1use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, FraiseQLError>;
11
12#[derive(Error, Debug)]
95#[non_exhaustive]
96pub enum FraiseQLError {
97 #[error("Parse error at {location}: {message}")]
102 Parse {
103 message: String,
105 location: String,
107 },
108
109 #[error("Validation error: {message}")]
111 Validation {
112 message: String,
114 path: Option<String>,
116 },
117
118 #[error("Unknown field '{field}' on type '{type_name}'")]
120 UnknownField {
121 field: String,
123 type_name: String,
125 },
126
127 #[error("Unknown type '{type_name}'")]
129 UnknownType {
130 type_name: String,
132 },
133
134 #[error("Database error: {message}")]
139 Database {
140 message: String,
142 sql_state: Option<String>,
144 },
145
146 #[error("Connection pool error: {message}")]
148 ConnectionPool {
149 message: String,
151 },
152
153 #[error("Query timeout after {timeout_ms}ms")]
155 Timeout {
156 timeout_ms: u64,
158 query: Option<String>,
160 },
161
162 #[error("Query cancelled: {reason}")]
164 Cancelled {
165 query_id: String,
167 reason: String,
169 },
170
171 #[error("Authorization error: {message}")]
176 Authorization {
177 message: String,
179 action: Option<String>,
181 resource: Option<String>,
183 },
184
185 #[error("Authentication error: {message}")]
187 Authentication {
188 message: String,
190 },
191
192 #[error("Rate limit exceeded: {message}")]
194 RateLimited {
195 message: String,
197 retry_after_secs: u64,
199 },
200
201 #[error("{resource_type} not found: {identifier}")]
206 NotFound {
207 resource_type: String,
209 identifier: String,
211 },
212
213 #[error("Conflict: {message}")]
215 Conflict {
216 message: String,
218 },
219
220 #[error("Configuration error: {message}")]
225 Configuration {
226 message: String,
228 },
229
230 #[error("Unsupported operation: {message}")]
232 Unsupported {
233 message: String,
235 },
236
237 #[error("Internal error: {message}")]
242 Internal {
243 message: String,
245 #[source]
247 source: Option<Box<dyn std::error::Error + Send + Sync>>,
248 },
249}
250
251impl FraiseQLError {
252 #[must_use]
254 pub fn parse(message: impl Into<String>) -> Self {
255 Self::Parse {
256 message: message.into(),
257 location: "unknown".to_string(),
258 }
259 }
260
261 #[must_use]
263 pub fn parse_at(message: impl Into<String>, location: impl Into<String>) -> Self {
264 Self::Parse {
265 message: message.into(),
266 location: location.into(),
267 }
268 }
269
270 #[must_use]
272 pub fn validation(message: impl Into<String>) -> Self {
273 Self::Validation {
274 message: message.into(),
275 path: None,
276 }
277 }
278
279 #[must_use]
281 pub fn validation_at(message: impl Into<String>, path: impl Into<String>) -> Self {
282 Self::Validation {
283 message: message.into(),
284 path: Some(path.into()),
285 }
286 }
287
288 #[must_use]
290 pub fn database(message: impl Into<String>) -> Self {
291 Self::Database {
292 message: message.into(),
293 sql_state: None,
294 }
295 }
296
297 #[must_use]
299 pub fn unauthorized(message: impl Into<String>) -> Self {
300 Self::Authorization {
301 message: message.into(),
302 action: None,
303 resource: None,
304 }
305 }
306
307 #[must_use]
309 pub fn not_found(resource_type: impl Into<String>, identifier: impl Into<String>) -> Self {
310 Self::NotFound {
311 resource_type: resource_type.into(),
312 identifier: identifier.into(),
313 }
314 }
315
316 #[must_use]
318 pub fn config(message: impl Into<String>) -> Self {
319 Self::Configuration {
320 message: message.into(),
321 }
322 }
323
324 #[must_use]
326 pub fn internal(message: impl Into<String>) -> Self {
327 Self::Internal {
328 message: message.into(),
329 source: None,
330 }
331 }
332
333 #[must_use]
335 pub fn cancelled(query_id: impl Into<String>, reason: impl Into<String>) -> Self {
336 Self::Cancelled {
337 query_id: query_id.into(),
338 reason: reason.into(),
339 }
340 }
341
342 #[must_use]
344 pub const fn is_client_error(&self) -> bool {
345 matches!(
346 self,
347 Self::Parse { .. }
348 | Self::Validation { .. }
349 | Self::UnknownField { .. }
350 | Self::UnknownType { .. }
351 | Self::Authorization { .. }
352 | Self::Authentication { .. }
353 | Self::NotFound { .. }
354 | Self::Conflict { .. }
355 | Self::RateLimited { .. }
356 )
357 }
358
359 #[must_use]
361 pub const fn is_server_error(&self) -> bool {
362 matches!(
363 self,
364 Self::Database { .. }
365 | Self::ConnectionPool { .. }
366 | Self::Timeout { .. }
367 | Self::Cancelled { .. }
368 | Self::Configuration { .. }
369 | Self::Unsupported { .. }
370 | Self::Internal { .. }
371 )
372 }
373
374 #[must_use]
376 pub const fn is_retryable(&self) -> bool {
377 matches!(
378 self,
379 Self::ConnectionPool { .. } | Self::Timeout { .. } | Self::Cancelled { .. }
380 )
381 }
382
383 #[must_use]
385 pub const fn status_code(&self) -> u16 {
386 match self {
387 Self::Parse { .. }
388 | Self::Validation { .. }
389 | Self::UnknownField { .. }
390 | Self::UnknownType { .. } => 400,
391 Self::Authentication { .. } => 401,
392 Self::Authorization { .. } => 403,
393 Self::NotFound { .. } => 404,
394 Self::Conflict { .. } => 409,
395 Self::RateLimited { .. } => 429,
396 Self::Timeout { .. } | Self::Cancelled { .. } => 408,
397 Self::Database { .. }
398 | Self::ConnectionPool { .. }
399 | Self::Configuration { .. }
400 | Self::Internal { .. } => 500,
401 Self::Unsupported { .. } => 501,
402 }
403 }
404
405 #[must_use]
407 pub const fn error_code(&self) -> &'static str {
408 match self {
409 Self::Parse { .. } => "GRAPHQL_PARSE_FAILED",
410 Self::Validation { .. } => "GRAPHQL_VALIDATION_FAILED",
411 Self::UnknownField { .. } => "UNKNOWN_FIELD",
412 Self::UnknownType { .. } => "UNKNOWN_TYPE",
413 Self::Database { .. } => "DATABASE_ERROR",
414 Self::ConnectionPool { .. } => "CONNECTION_POOL_ERROR",
415 Self::Timeout { .. } => "TIMEOUT",
416 Self::Cancelled { .. } => "CANCELLED",
417 Self::Authorization { .. } => "FORBIDDEN",
418 Self::Authentication { .. } => "UNAUTHENTICATED",
419 Self::RateLimited { .. } => "RATE_LIMITED",
420 Self::NotFound { .. } => "NOT_FOUND",
421 Self::Conflict { .. } => "CONFLICT",
422 Self::Configuration { .. } => "CONFIGURATION_ERROR",
423 Self::Unsupported { .. } => "UNSUPPORTED_OPERATION",
424 Self::Internal { .. } => "INTERNAL_SERVER_ERROR",
425 }
426 }
427
428 #[must_use]
430 pub fn unknown_field_with_suggestion(
431 field: impl Into<String>,
432 type_name: impl Into<String>,
433 available_fields: &[&str],
434 ) -> Self {
435 let field = field.into();
436 let type_name = type_name.into();
437
438 let suggestion = available_fields
439 .iter()
440 .map(|f| (*f, Self::levenshtein_distance(&field, f)))
441 .filter(|(_, distance)| *distance <= 2)
442 .min_by_key(|(_, distance)| *distance)
443 .map(|(f, _)| f);
444
445 if let Some(suggested_field) = suggestion {
446 Self::UnknownField {
447 field: format!("{field} (did you mean '{suggested_field}'?)"),
448 type_name,
449 }
450 } else {
451 Self::UnknownField { field, type_name }
452 }
453 }
454
455 fn levenshtein_distance(s1: &str, s2: &str) -> usize {
456 let len1 = s1.chars().count();
457 let len2 = s2.chars().count();
458
459 if len1 == 0 {
460 return len2;
461 }
462 if len2 == 0 {
463 return len1;
464 }
465
466 let mut matrix = vec![vec![0; len2 + 1]; len1 + 1];
467
468 for (i, row) in matrix.iter_mut().enumerate() {
469 row[0] = i;
470 }
471 for (j, val) in matrix[0].iter_mut().enumerate() {
472 *val = j;
473 }
474
475 for (i, c1) in s1.chars().enumerate() {
476 for (j, c2) in s2.chars().enumerate() {
477 let cost = usize::from(c1 != c2);
478 matrix[i + 1][j + 1] = std::cmp::min(
479 std::cmp::min(matrix[i][j + 1] + 1, matrix[i + 1][j] + 1),
480 matrix[i][j] + cost,
481 );
482 }
483 }
484
485 matrix[len1][len2]
486 }
487
488 #[must_use]
490 pub fn from_postgres_code(code: &str, message: impl Into<String>) -> Self {
491 let message = message.into();
492 match code {
493 "42P01" => Self::Database {
494 message: "The table or view you're querying doesn't exist. \
495 Check that the schema is compiled and the database is initialized."
496 .to_string(),
497 sql_state: Some(code.to_string()),
498 },
499 "42703" => Self::Database {
500 message: "A column referenced in the query doesn't exist in the table. \
501 This may indicate the database schema is out of sync with the compiled schema."
502 .to_string(),
503 sql_state: Some(code.to_string()),
504 },
505 "23505" => Self::Conflict {
506 message: "A unique constraint was violated. This value already exists in the database.".to_string(),
507 },
508 "23503" => Self::Conflict {
509 message: "A foreign key constraint was violated. The referenced record doesn't exist."
510 .to_string(),
511 },
512 "23502" => Self::Conflict {
513 message: "A NOT NULL constraint was violated. The field cannot be empty.".to_string(),
514 },
515 "22P02" => Self::Validation {
516 message: "Invalid input value. The provided value doesn't match the expected data type.".to_string(),
517 path: None,
518 },
519 _ => Self::Database {
520 message,
521 sql_state: Some(code.to_string()),
522 },
523 }
524 }
525
526 #[must_use]
528 pub fn rate_limited_with_retry(retry_after_secs: u64) -> Self {
529 Self::RateLimited {
530 message: format!(
531 "Rate limit exceeded. Please try again in {retry_after_secs} seconds. \
532 For permanent increases, contact support."
533 ),
534 retry_after_secs,
535 }
536 }
537
538 #[must_use]
540 pub fn auth_error(reason: impl Into<String>) -> Self {
541 Self::Authentication {
542 message: reason.into(),
543 }
544 }
545}
546
547impl From<serde_json::Error> for FraiseQLError {
548 fn from(e: serde_json::Error) -> Self {
549 Self::Parse {
550 message: e.to_string(),
551 location: format!("line {}, column {}", e.line(), e.column()),
552 }
553 }
554}
555
556impl From<std::io::Error> for FraiseQLError {
557 fn from(e: std::io::Error) -> Self {
558 Self::Internal {
559 message: format!("I/O error: {e}"),
560 source: Some(Box::new(e)),
561 }
562 }
563}
564
565impl From<std::env::VarError> for FraiseQLError {
566 fn from(e: std::env::VarError) -> Self {
567 Self::Configuration {
568 message: format!("Environment variable error: {e}"),
569 }
570 }
571}
572
573pub trait ErrorContext<T> {
575 fn context(self, message: impl Into<String>) -> Result<T>;
582
583 fn with_context<F, M>(self, f: F) -> Result<T>
590 where
591 F: FnOnce() -> M,
592 M: Into<String>;
593}
594
595impl<T, E: Into<FraiseQLError>> ErrorContext<T> for std::result::Result<T, E> {
596 fn context(self, message: impl Into<String>) -> Result<T> {
597 self.map_err(|e| {
598 let inner = e.into();
599 FraiseQLError::Internal {
600 message: format!("{}: {inner}", message.into()),
601 source: None,
602 }
603 })
604 }
605
606 fn with_context<F, M>(self, f: F) -> Result<T>
607 where
608 F: FnOnce() -> M,
609 M: Into<String>,
610 {
611 self.map_err(|e| {
612 let inner = e.into();
613 FraiseQLError::Internal {
614 message: format!("{}: {inner}", f().into()),
615 source: None,
616 }
617 })
618 }
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623pub struct ValidationFieldError {
624 pub field: String,
626 pub rule_type: String,
628 pub message: String,
630}
631
632impl ValidationFieldError {
633 #[must_use]
635 pub fn new(
636 field: impl Into<String>,
637 rule_type: impl Into<String>,
638 message: impl Into<String>,
639 ) -> Self {
640 Self {
641 field: field.into(),
642 rule_type: rule_type.into(),
643 message: message.into(),
644 }
645 }
646}
647
648impl std::fmt::Display for ValidationFieldError {
649 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650 write!(f, "{} ({}): {}", self.field, self.rule_type, self.message)
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn test_parse_error() {
660 let err = FraiseQLError::parse("unexpected token");
661 assert!(err.is_client_error());
662 assert!(!err.is_server_error());
663 assert_eq!(err.status_code(), 400);
664 assert_eq!(err.error_code(), "GRAPHQL_PARSE_FAILED");
665 }
666
667 #[test]
668 fn test_database_error() {
669 let err = FraiseQLError::database("connection refused");
670 assert!(!err.is_client_error());
671 assert!(err.is_server_error());
672 assert_eq!(err.status_code(), 500);
673 }
674
675 #[test]
676 fn test_not_found_error() {
677 let err = FraiseQLError::not_found("User", "123");
678 assert!(err.is_client_error());
679 assert_eq!(err.status_code(), 404);
680 assert_eq!(err.to_string(), "User not found: 123");
681 }
682
683 #[test]
684 fn test_retryable_errors() {
685 assert!(
686 FraiseQLError::ConnectionPool {
687 message: "timeout".to_string(),
688 }
689 .is_retryable()
690 );
691 assert!(
692 FraiseQLError::Timeout {
693 timeout_ms: 5000,
694 query: None,
695 }
696 .is_retryable()
697 );
698 assert!(!FraiseQLError::parse("bad query").is_retryable());
699 }
700
701 #[test]
702 fn test_unsupported_is_501() {
703 let err = FraiseQLError::Unsupported {
704 message: "execute_function_call not supported on SQLite".to_string(),
705 };
706 assert_eq!(err.status_code(), 501);
707 assert!(err.is_server_error());
708 assert_eq!(err.error_code(), "UNSUPPORTED_OPERATION");
709 }
710
711 #[test]
712 fn test_from_serde_error() {
713 let json_err = serde_json::from_str::<serde_json::Value>("not json")
714 .expect_err("'not json' must fail to parse");
715 let err: FraiseQLError = json_err.into();
716 assert!(matches!(err, FraiseQLError::Parse { .. }));
717 }
718
719 #[test]
720 fn test_validation_field_error_creation() {
721 let field_err = ValidationFieldError::new("user.email", "pattern", "Invalid email format");
722 assert_eq!(field_err.field, "user.email");
723 assert_eq!(field_err.rule_type, "pattern");
724 assert_eq!(field_err.message, "Invalid email format");
725 }
726
727 #[test]
728 fn test_levenshtein_ascii() {
729 assert_eq!(FraiseQLError::levenshtein_distance("kitten", "sitting"), 3);
731 assert_eq!(FraiseQLError::levenshtein_distance("", "abc"), 3);
732 assert_eq!(FraiseQLError::levenshtein_distance("abc", ""), 3);
733 assert_eq!(FraiseQLError::levenshtein_distance("same", "same"), 0);
734 }
735
736 #[test]
737 fn test_levenshtein_multibyte_utf8() {
738 assert_eq!(FraiseQLError::levenshtein_distance("café", "cafe"), 1);
741 assert_eq!(FraiseQLError::levenshtein_distance("naïve", "naive"), 1);
742 assert_eq!(FraiseQLError::levenshtein_distance("café", "café"), 0);
744 }
745}