fraiseql_core/validation/
error_responses.rs1use serde::{Deserialize, Serialize};
7
8use crate::error::{FraiseQLError, ValidationFieldError};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GraphQLValidationError {
13 pub message: String,
15
16 #[serde(skip_serializing_if = "Option::is_none")]
18 pub path: Option<Vec<String>>,
19
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub extensions: Option<ValidationErrorExtensions>,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ValidationErrorExtensions {
28 pub code: String,
30
31 pub rule_type: String,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub field_path: Option<String>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub context: Option<serde_json::Value>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GraphQLValidationResponse {
46 pub errors: Vec<GraphQLValidationError>,
48
49 pub error_count: usize,
51}
52
53impl GraphQLValidationResponse {
54 pub const fn new() -> Self {
56 Self {
57 errors: Vec::new(),
58 error_count: 0,
59 }
60 }
61
62 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 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 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 fn parse_path(path: &str) -> Vec<String> {
115 path.split('.').map(|s| s.to_string()).collect()
116 }
117
118 pub const fn has_errors(&self) -> bool {
120 !self.errors.is_empty()
121 }
122
123 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 #![allow(clippy::unwrap_used)] use super::*;
143
144 #[test]
145 fn test_create_empty_response() {
146 let response = GraphQLValidationResponse::new();
147 assert!(!response.has_errors());
148 assert_eq!(response.error_count, 0);
149 }
150
151 #[test]
152 fn test_add_single_error() {
153 let mut response = GraphQLValidationResponse::new();
154 let field_error = ValidationFieldError::new("email", "pattern", "Invalid email format");
155 response.add_field_error(field_error, None);
156
157 assert!(response.has_errors());
158 assert_eq!(response.error_count, 1);
159 assert_eq!(response.errors[0].extensions.as_ref().unwrap().rule_type, "pattern");
160 }
161
162 #[test]
163 fn test_add_multiple_errors() {
164 let mut response = GraphQLValidationResponse::new();
165 let errors = vec![
166 ValidationFieldError::new("email", "pattern", "Invalid email"),
167 ValidationFieldError::new("phone", "pattern", "Invalid phone"),
168 ];
169 response.add_errors(errors);
170
171 assert_eq!(response.error_count, 2);
172 }
173
174 #[test]
175 fn test_path_parsing() {
176 let path = GraphQLValidationResponse::parse_path("user.email");
177 assert_eq!(path, vec!["user".to_string(), "email".to_string()]);
178
179 let path = GraphQLValidationResponse::parse_path("address.zipcode");
180 assert_eq!(path, vec!["address".to_string(), "zipcode".to_string()]);
181 }
182
183 #[test]
184 fn test_json_serialization() {
185 let mut response = GraphQLValidationResponse::new();
186 let field_error = ValidationFieldError::new("field1", "rule1", "Error message");
187 response.add_field_error(field_error, Some(serde_json::json!({"detail": "extra"})));
188
189 let json = response.to_graphql_errors();
190 assert!(json["error_count"].is_number());
191 assert!(json["errors"].is_array());
192 }
193
194 #[test]
195 fn test_from_fraiseql_error() {
196 let error = FraiseQLError::Validation {
197 message: "Validation failed".to_string(),
198 path: Some("user.email".to_string()),
199 };
200
201 let response = GraphQLValidationResponse::from_error(&error);
202 assert!(response.is_some());
203 let response = response.unwrap();
204 assert_eq!(response.error_count, 1);
205 }
206
207 #[test]
208 fn test_context_inclusion() {
209 let mut response = GraphQLValidationResponse::new();
210 let field_error = ValidationFieldError::new("password", "length", "Too short");
211 let context = serde_json::json!({"minimum_length": 12, "provided_length": 8});
212 response.add_field_error(field_error, Some(context));
213
214 assert!(response.errors[0].extensions.as_ref().unwrap().context.is_some());
215 }
216}