Skip to main content

fraiseql_core/validation/
error_responses.rs

1//! GraphQL-compliant error response formatting for validation errors.
2//!
3//! This module provides utilities for converting validation errors into
4//! GraphQL error responses with proper structure and context.
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::{FraiseQLError, ValidationFieldError};
9
10/// A GraphQL error with extensions (validation details).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GraphQLValidationError {
13    /// Error message shown to client
14    pub message: String,
15
16    /// Path to the field with error (e.g., "createUser.input.email")
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub path: Option<Vec<String>>,
19
20    /// Extensions with validation details
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub extensions: Option<ValidationErrorExtensions>,
23}
24
25/// Extensions carrying validation-specific error details.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ValidationErrorExtensions {
28    /// GraphQL error code
29    pub code: String,
30
31    /// Human-readable rule type that failed
32    pub rule_type: String,
33
34    /// Field path as dot-separated string
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub field_path: Option<String>,
37
38    /// Additional context (e.g., why pattern didn't match)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub context: Option<serde_json::Value>,
41}
42
43/// Collection of GraphQL validation errors.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GraphQLValidationResponse {
46    /// List of validation errors
47    pub errors: Vec<GraphQLValidationError>,
48
49    /// Total error count
50    pub error_count: usize,
51}
52
53impl GraphQLValidationResponse {
54    /// Create a new empty error response.
55    #[must_use]
56    pub const fn new() -> Self {
57        Self {
58            errors:      Vec::new(),
59            error_count: 0,
60        }
61    }
62
63    /// Add a validation field error to the response.
64    pub fn add_field_error(
65        &mut self,
66        field_error: ValidationFieldError,
67        context: Option<serde_json::Value>,
68    ) {
69        let path = Self::parse_path(&field_error.field);
70        let extensions = ValidationErrorExtensions {
71            code: "VALIDATION_FAILED".to_string(),
72            rule_type: field_error.rule_type,
73            field_path: Some(field_error.field.clone()),
74            context,
75        };
76
77        self.errors.push(GraphQLValidationError {
78            message:    format!("Validation failed: {}", field_error.message),
79            path:       Some(path),
80            extensions: Some(extensions),
81        });
82
83        self.error_count += 1;
84    }
85
86    /// Add multiple validation errors at once.
87    pub fn add_errors(&mut self, errors: Vec<ValidationFieldError>) {
88        for error in errors {
89            self.add_field_error(error, None);
90        }
91    }
92
93    /// Convert from `FraiseQLError` to validation response.
94    #[must_use]
95    pub fn from_error(error: &FraiseQLError) -> Option<Self> {
96        if let FraiseQLError::Validation { message, path } = error {
97            let mut response = Self::new();
98            response.errors.push(GraphQLValidationError {
99                message:    message.clone(),
100                path:       path.as_ref().map(|p| Self::parse_path(p)),
101                extensions: Some(ValidationErrorExtensions {
102                    code:       "VALIDATION_FAILED".to_string(),
103                    rule_type:  "unknown".to_string(),
104                    field_path: path.clone(),
105                    context:    None,
106                }),
107            });
108            response.error_count = 1;
109            Some(response)
110        } else {
111            None
112        }
113    }
114
115    /// Parse a dot-separated field path into path segments.
116    pub(crate) fn parse_path(path: &str) -> Vec<String> {
117        path.split('.').map(|s| s.to_string()).collect()
118    }
119
120    /// Check if response has any errors.
121    #[must_use]
122    pub const fn has_errors(&self) -> bool {
123        !self.errors.is_empty()
124    }
125
126    /// Serialize to JSON suitable for GraphQL response.
127    #[must_use]
128    pub fn to_graphql_errors(&self) -> serde_json::Value {
129        serde_json::json!({
130            "errors": self.errors,
131            "error_count": self.error_count
132        })
133    }
134}
135
136impl Default for GraphQLValidationResponse {
137    fn default() -> Self {
138        Self::new()
139    }
140}