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    pub fn new() -> Self {
56        Self {
57            errors:      Vec::new(),
58            error_count: 0,
59        }
60    }
61
62    /// Add a validation field error to the response.
63    pub fn add_field_error(
64        &mut self,
65        field_error: ValidationFieldError,
66        context: Option<serde_json::Value>,
67    ) {
68        let path = Self::parse_path(&field_error.field);
69        let extensions = ValidationErrorExtensions {
70            code: "VALIDATION_FAILED".to_string(),
71            rule_type: field_error.rule_type,
72            field_path: Some(field_error.field.clone()),
73            context,
74        };
75
76        self.errors.push(GraphQLValidationError {
77            message:    format!("Validation failed: {}", field_error.message),
78            path:       Some(path),
79            extensions: Some(extensions),
80        });
81
82        self.error_count += 1;
83    }
84
85    /// Add multiple validation errors at once.
86    pub fn add_errors(&mut self, errors: Vec<ValidationFieldError>) {
87        for error in errors {
88            self.add_field_error(error, None);
89        }
90    }
91
92    /// Convert from FraiseQLError to validation response.
93    pub fn from_error(error: &FraiseQLError) -> Option<Self> {
94        if let FraiseQLError::Validation { message, path } = error {
95            let mut response = Self::new();
96            response.errors.push(GraphQLValidationError {
97                message:    message.clone(),
98                path:       path.as_ref().map(|p| Self::parse_path(p)),
99                extensions: Some(ValidationErrorExtensions {
100                    code:       "VALIDATION_FAILED".to_string(),
101                    rule_type:  "unknown".to_string(),
102                    field_path: path.clone(),
103                    context:    None,
104                }),
105            });
106            response.error_count = 1;
107            Some(response)
108        } else {
109            None
110        }
111    }
112
113    /// Parse a dot-separated field path into path segments.
114    fn parse_path(path: &str) -> Vec<String> {
115        path.split('.').map(|s| s.to_string()).collect()
116    }
117
118    /// Check if response has any errors.
119    pub fn has_errors(&self) -> bool {
120        !self.errors.is_empty()
121    }
122
123    /// Serialize to JSON suitable for GraphQL response.
124    pub fn to_graphql_errors(&self) -> serde_json::Value {
125        serde_json::json!({
126            "errors": self.errors,
127            "error_count": self.error_count
128        })
129    }
130}
131
132impl Default for GraphQLValidationResponse {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_create_empty_response() {
144        let response = GraphQLValidationResponse::new();
145        assert!(!response.has_errors());
146        assert_eq!(response.error_count, 0);
147    }
148
149    #[test]
150    fn test_add_single_error() {
151        let mut response = GraphQLValidationResponse::new();
152        let field_error = ValidationFieldError::new("email", "pattern", "Invalid email format");
153        response.add_field_error(field_error, None);
154
155        assert!(response.has_errors());
156        assert_eq!(response.error_count, 1);
157        assert_eq!(response.errors[0].extensions.as_ref().unwrap().rule_type, "pattern");
158    }
159
160    #[test]
161    fn test_add_multiple_errors() {
162        let mut response = GraphQLValidationResponse::new();
163        let errors = vec![
164            ValidationFieldError::new("email", "pattern", "Invalid email"),
165            ValidationFieldError::new("phone", "pattern", "Invalid phone"),
166        ];
167        response.add_errors(errors);
168
169        assert_eq!(response.error_count, 2);
170    }
171
172    #[test]
173    fn test_path_parsing() {
174        let path = GraphQLValidationResponse::parse_path("user.email");
175        assert_eq!(path, vec!["user".to_string(), "email".to_string()]);
176
177        let path = GraphQLValidationResponse::parse_path("address.zipcode");
178        assert_eq!(path, vec!["address".to_string(), "zipcode".to_string()]);
179    }
180
181    #[test]
182    fn test_json_serialization() {
183        let mut response = GraphQLValidationResponse::new();
184        let field_error = ValidationFieldError::new("field1", "rule1", "Error message");
185        response.add_field_error(field_error, Some(serde_json::json!({"detail": "extra"})));
186
187        let json = response.to_graphql_errors();
188        assert!(json["error_count"].is_number());
189        assert!(json["errors"].is_array());
190    }
191
192    #[test]
193    fn test_from_fraiseql_error() {
194        let error = FraiseQLError::Validation {
195            message: "Validation failed".to_string(),
196            path:    Some("user.email".to_string()),
197        };
198
199        let response = GraphQLValidationResponse::from_error(&error);
200        assert!(response.is_some());
201        let response = response.unwrap();
202        assert_eq!(response.error_count, 1);
203    }
204
205    #[test]
206    fn test_context_inclusion() {
207        let mut response = GraphQLValidationResponse::new();
208        let field_error = ValidationFieldError::new("password", "length", "Too short");
209        let context = serde_json::json!({"minimum_length": 12, "provided_length": 8});
210        response.add_field_error(field_error, Some(context));
211
212        assert!(response.errors[0].extensions.as_ref().unwrap().context.is_some());
213    }
214}