1use serde_json::Value;
8
9use crate::{
10 error::{FraiseQLError, Result, ValidationFieldError},
11 schema::CompiledSchema,
12 validation::ValidationRule,
13};
14
15#[derive(Debug, Clone, Default)]
17pub struct ValidationErrorCollection {
18 pub errors: Vec<ValidationFieldError>,
20}
21
22impl ValidationErrorCollection {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn add_error(&mut self, error: ValidationFieldError) {
30 self.errors.push(error);
31 }
32
33 pub fn is_empty(&self) -> bool {
35 self.errors.is_empty()
36 }
37
38 pub fn len(&self) -> usize {
40 self.errors.len()
41 }
42
43 pub fn to_error(&self) -> FraiseQLError {
45 if self.errors.is_empty() {
46 FraiseQLError::validation("No validation errors")
47 } else if self.errors.len() == 1 {
48 let err = &self.errors[0];
49 FraiseQLError::Validation {
50 message: err.to_string(),
51 path: Some(err.field.clone()),
52 }
53 } else {
54 let messages: Vec<String> = self.errors.iter().map(|e| e.to_string()).collect();
55 FraiseQLError::Validation {
56 message: format!("Multiple validation errors: {}", messages.join("; ")),
57 path: None,
58 }
59 }
60 }
61}
62
63pub fn validate_custom_scalar(
78 value: &Value,
79 scalar_type_name: &str,
80 schema: &CompiledSchema,
81) -> Result<()> {
82 if schema.custom_scalars.exists(scalar_type_name) {
84 schema.custom_scalars.validate(scalar_type_name, value)
85 } else {
86 Ok(())
88 }
89}
90
91pub fn validate_input(value: &Value, field_path: &str, rules: &[ValidationRule]) -> Result<()> {
96 let mut errors = ValidationErrorCollection::new();
97
98 match value {
99 Value::String(s) => {
100 for rule in rules {
101 if let Err(FraiseQLError::Validation { message, .. }) =
102 validate_string_field(s, field_path, rule)
103 {
104 if let Some(field_err) = extract_field_error(&message) {
105 errors.add_error(field_err);
106 }
107 }
108 }
109 },
110 Value::Null => {
111 for rule in rules {
112 if rule.is_required() {
113 errors.add_error(ValidationFieldError::new(
114 field_path,
115 "required",
116 "Field is required",
117 ));
118 }
119 }
120 },
121 _ => {
122 },
124 }
125
126 if errors.is_empty() {
127 Ok(())
128 } else {
129 Err(errors.to_error())
130 }
131}
132
133fn validate_string_field(value: &str, field_path: &str, rule: &ValidationRule) -> Result<()> {
135 match rule {
136 ValidationRule::Required => {
137 if value.is_empty() {
138 return Err(FraiseQLError::Validation {
139 message: format!(
140 "Field validation failed: {}",
141 ValidationFieldError::new(field_path, "required", "Field is required")
142 ),
143 path: Some(field_path.to_string()),
144 });
145 }
146 Ok(())
147 },
148 ValidationRule::Pattern { pattern, message } => {
149 let regex = regex::Regex::new(pattern)
150 .map_err(|e| FraiseQLError::validation(format!("Invalid regex pattern: {}", e)))?;
151 if regex.is_match(value) {
152 Ok(())
153 } else {
154 let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
155 Err(FraiseQLError::Validation {
156 message: format!(
157 "Field validation failed: {}",
158 ValidationFieldError::new(field_path, "pattern", msg)
159 ),
160 path: Some(field_path.to_string()),
161 })
162 }
163 },
164 ValidationRule::Length { min, max } => {
165 let len = value.len();
166 let valid = if let Some(m) = min { len >= *m } else { true }
167 && if let Some(m) = max { len <= *m } else { true };
168
169 if valid {
170 Ok(())
171 } else {
172 let msg = match (min, max) {
173 (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
174 (Some(m), None) => format!("Length must be at least {}", m),
175 (None, Some(x)) => format!("Length must be at most {}", x),
176 (None, None) => "Length validation failed".to_string(),
177 };
178 Err(FraiseQLError::Validation {
179 message: format!(
180 "Field validation failed: {}",
181 ValidationFieldError::new(field_path, "length", msg)
182 ),
183 path: Some(field_path.to_string()),
184 })
185 }
186 },
187 ValidationRule::Enum { values } => {
188 if values.contains(&value.to_string()) {
189 Ok(())
190 } else {
191 Err(FraiseQLError::Validation {
192 message: format!(
193 "Field validation failed: {}",
194 ValidationFieldError::new(
195 field_path,
196 "enum",
197 format!("Must be one of: {}", values.join(", "))
198 )
199 ),
200 path: Some(field_path.to_string()),
201 })
202 }
203 },
204 _ => Ok(()), }
206}
207
208fn extract_field_error(message: &str) -> Option<ValidationFieldError> {
210 if message.contains("Field validation failed:") {
212 if let Some(field_start) = message.find("Field validation failed: ") {
213 let rest = &message[field_start + "Field validation failed: ".len()..];
214 if let Some(paren_start) = rest.find('(') {
215 if let Some(paren_end) = rest.find(')') {
216 let field = rest[..paren_start].trim().to_string();
217 let rule_type = rest[paren_start + 1..paren_end].to_string();
218 let msg_start = rest.find(": ").unwrap_or(0) + 2;
219 let message_text = rest[msg_start..].to_string();
220 return Some(ValidationFieldError::new(field, rule_type, message_text));
221 }
222 }
223 }
224 }
225 None
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_validation_error_collection() {
234 let mut errors = ValidationErrorCollection::new();
235 assert!(errors.is_empty());
236
237 errors.add_error(ValidationFieldError::new("email", "pattern", "Invalid email"));
238 assert!(!errors.is_empty());
239 assert_eq!(errors.len(), 1);
240 }
241
242 #[test]
243 fn test_validation_error_collection_to_error() {
244 let mut errors = ValidationErrorCollection::new();
245 errors.add_error(ValidationFieldError::new("email", "pattern", "Invalid email"));
246
247 let err = errors.to_error();
248 assert!(matches!(err, FraiseQLError::Validation { .. }));
249 }
250
251 #[test]
252 fn test_validate_required_field() {
253 let rule = ValidationRule::Required;
254 let result = validate_string_field("value", "field", &rule);
255 assert!(result.is_ok());
256
257 let result = validate_string_field("", "field", &rule);
258 assert!(result.is_err());
259 }
260
261 #[test]
262 fn test_validate_pattern() {
263 let rule = ValidationRule::Pattern {
264 pattern: "^[a-z]+$".to_string(),
265 message: None,
266 };
267
268 let result = validate_string_field("hello", "field", &rule);
269 assert!(result.is_ok());
270
271 let result = validate_string_field("Hello", "field", &rule);
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_validate_length() {
277 let rule = ValidationRule::Length {
278 min: Some(3),
279 max: Some(10),
280 };
281
282 let result = validate_string_field("hello", "field", &rule);
283 assert!(result.is_ok());
284
285 let result = validate_string_field("hi", "field", &rule);
286 assert!(result.is_err());
287
288 let result = validate_string_field("this is too long", "field", &rule);
289 assert!(result.is_err());
290 }
291
292 #[test]
293 fn test_validate_enum() {
294 let rule = ValidationRule::Enum {
295 values: vec!["active".to_string(), "inactive".to_string()],
296 };
297
298 let result = validate_string_field("active", "field", &rule);
299 assert!(result.is_ok());
300
301 let result = validate_string_field("unknown", "field", &rule);
302 assert!(result.is_err());
303 }
304
305 #[test]
306 fn test_validate_null_field() {
307 let rule = ValidationRule::Required;
308 let result = validate_input(&Value::Null, "field", &[rule]);
309 assert!(result.is_err());
310 }
311
312 #[test]
313 fn test_validate_custom_scalar_library_code_valid() {
314 use crate::{
315 schema::CompiledSchema,
316 validation::{CustomTypeDef, CustomTypeRegistry},
317 };
318
319 let schema = {
320 let mut s = CompiledSchema::new();
321 let registry = CustomTypeRegistry::new(Default::default());
322
323 let mut def = CustomTypeDef::new("LibraryCode".to_string());
324 def.validation_rules = vec![ValidationRule::Pattern {
325 pattern: r"^LIB-[0-9]{4}$".to_string(),
326 message: Some("Library code must be LIB-#### format".to_string()),
327 }];
328
329 registry.register("LibraryCode".to_string(), def).unwrap();
330
331 s.custom_scalars = registry;
332 s
333 };
334
335 let value = serde_json::json!("LIB-1234");
336 let result = validate_custom_scalar(&value, "LibraryCode", &schema);
337 assert!(result.is_ok());
338 }
339
340 #[test]
341 fn test_validate_custom_scalar_library_code_invalid() {
342 use crate::{
343 schema::CompiledSchema,
344 validation::{CustomTypeDef, CustomTypeRegistry},
345 };
346
347 let schema = {
348 let mut s = CompiledSchema::new();
349 let registry = CustomTypeRegistry::new(Default::default());
350
351 let mut def = CustomTypeDef::new("LibraryCode".to_string());
352 def.validation_rules = vec![ValidationRule::Pattern {
353 pattern: r"^LIB-[0-9]{4}$".to_string(),
354 message: Some("Library code must be LIB-#### format".to_string()),
355 }];
356
357 registry.register("LibraryCode".to_string(), def).unwrap();
358
359 s.custom_scalars = registry;
360 s
361 };
362
363 let value = serde_json::json!("INVALID");
364 let result = validate_custom_scalar(&value, "LibraryCode", &schema);
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_validate_custom_scalar_student_id_with_length() {
370 use crate::{
371 schema::CompiledSchema,
372 validation::{CustomTypeDef, CustomTypeRegistry},
373 };
374
375 let schema = {
376 let mut s = CompiledSchema::new();
377 let registry = CustomTypeRegistry::new(Default::default());
378
379 let mut def = CustomTypeDef::new("StudentID".to_string());
380 def.validation_rules = vec![
381 ValidationRule::Pattern {
382 pattern: r"^STU-[0-9]{4}-[0-9]{3}$".to_string(),
383 message: None,
384 },
385 ValidationRule::Length {
386 min: Some(12),
387 max: Some(12),
388 },
389 ];
390
391 registry.register("StudentID".to_string(), def).unwrap();
392
393 s.custom_scalars = registry;
394 s
395 };
396
397 let value = serde_json::json!("STU-2024-001");
399 let result = validate_custom_scalar(&value, "StudentID", &schema);
400 assert!(result.is_ok());
401
402 let value = serde_json::json!("STUDENT-2024");
404 let result = validate_custom_scalar(&value, "StudentID", &schema);
405 assert!(result.is_err());
406 }
407
408 #[test]
409 fn test_validate_unknown_scalar_type_passthrough() {
410 use crate::schema::CompiledSchema;
411
412 let schema = CompiledSchema::new();
413
414 let value = serde_json::json!("any value");
416 let result = validate_custom_scalar(&value, "UnknownType", &schema);
417 assert!(result.is_ok());
418 }
419
420 #[test]
421 fn test_validate_custom_scalar_patient_id_passthrough() {
422 use crate::schema::CompiledSchema;
423
424 let schema = CompiledSchema::new();
426
427 let value = serde_json::json!("PAT-123456");
428 let result = validate_custom_scalar(&value, "PatientID", &schema);
429 assert!(result.is_ok());
431 }
432
433 #[test]
434 fn test_validate_custom_scalar_with_elo_expression() {
435 use crate::{
436 schema::CompiledSchema,
437 validation::{CustomTypeDef, CustomTypeRegistry},
438 };
439
440 let schema = {
441 let mut s = CompiledSchema::new();
442 let registry = CustomTypeRegistry::new(Default::default());
443
444 let mut def = CustomTypeDef::new("StudentID".to_string());
445 def.elo_expression = Some("matches(value, \"^STU-[0-9]{4}-[0-9]{3}$\")".to_string());
446
447 registry.register("StudentID".to_string(), def).unwrap();
448
449 s.custom_scalars = registry;
450 s
451 };
452
453 let value = serde_json::json!("STU-2024-001");
455 let result = validate_custom_scalar(&value, "StudentID", &schema);
456 assert!(result.is_ok());
457
458 let value = serde_json::json!("INVALID");
460 let result = validate_custom_scalar(&value, "StudentID", &schema);
461 assert!(result.is_err());
462 }
463
464 #[test]
465 fn test_validate_custom_scalar_combined_rules_and_elo() {
466 use crate::{
467 schema::CompiledSchema,
468 validation::{CustomTypeDef, CustomTypeRegistry},
469 };
470
471 let schema = {
472 let mut s = CompiledSchema::new();
473 let registry = CustomTypeRegistry::new(Default::default());
474
475 let mut def = CustomTypeDef::new("PatientID".to_string());
476 def.validation_rules = vec![ValidationRule::Length {
477 min: Some(10),
478 max: Some(10),
479 }];
480 def.elo_expression = Some("matches(value, \"^PAT-[0-9]{6}$\")".to_string());
481
482 registry.register("PatientID".to_string(), def).unwrap();
483
484 s.custom_scalars = registry;
485 s
486 };
487
488 let value = serde_json::json!("PAT-123456");
490 let result = validate_custom_scalar(&value, "PatientID", &schema);
491 assert!(result.is_ok());
492
493 let value = serde_json::json!("NOTVALID!");
495 let result = validate_custom_scalar(&value, "PatientID", &schema);
496 assert!(result.is_err());
497
498 let value = serde_json::json!("PAT-12345");
500 let result = validate_custom_scalar(&value, "PatientID", &schema);
501 assert!(result.is_err());
502 }
503}