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 const fn is_empty(&self) -> bool {
35 self.errors.is_empty()
36 }
37
38 pub const 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_from_schema(
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<()> {
101 let mut errors = ValidationErrorCollection::new();
102
103 match value {
104 Value::String(s) => {
105 for rule in rules {
106 if let Err(FraiseQLError::Validation { message, .. }) =
107 validate_string_field(s, field_path, rule)
108 {
109 if let Some(field_err) = extract_field_error(&message) {
110 errors.add_error(field_err);
111 }
112 }
113 }
114 },
115 Value::Null => {
116 for rule in rules {
117 if rule.is_required() {
118 errors.add_error(ValidationFieldError::new(
119 field_path,
120 "required",
121 "Field is required",
122 ));
123 }
124 }
125 },
126 _ => {
127 },
129 }
130
131 if errors.is_empty() {
132 Ok(())
133 } else {
134 Err(errors.to_error())
135 }
136}
137
138fn validate_string_field(value: &str, field_path: &str, rule: &ValidationRule) -> Result<()> {
140 match rule {
141 ValidationRule::Required => {
142 if value.is_empty() {
143 return Err(FraiseQLError::Validation {
144 message: format!(
145 "Field validation failed: {}",
146 ValidationFieldError::new(field_path, "required", "Field is required")
147 ),
148 path: Some(field_path.to_string()),
149 });
150 }
151 Ok(())
152 },
153 ValidationRule::Pattern { pattern, message } => {
154 let regex = regex::Regex::new(pattern)
155 .map_err(|e| FraiseQLError::validation(format!("Invalid regex pattern: {}", e)))?;
156 if regex.is_match(value) {
157 Ok(())
158 } else {
159 let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
160 Err(FraiseQLError::Validation {
161 message: format!(
162 "Field validation failed: {}",
163 ValidationFieldError::new(field_path, "pattern", msg)
164 ),
165 path: Some(field_path.to_string()),
166 })
167 }
168 },
169 ValidationRule::Length { min, max } => {
170 let len = value.len();
171 let valid = if let Some(m) = min { len >= *m } else { true }
172 && if let Some(m) = max { len <= *m } else { true };
173
174 if valid {
175 Ok(())
176 } else {
177 let msg = match (min, max) {
178 (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
179 (Some(m), None) => format!("Length must be at least {}", m),
180 (None, Some(x)) => format!("Length must be at most {}", x),
181 (None, None) => "Length validation failed".to_string(),
182 };
183 Err(FraiseQLError::Validation {
184 message: format!(
185 "Field validation failed: {}",
186 ValidationFieldError::new(field_path, "length", msg)
187 ),
188 path: Some(field_path.to_string()),
189 })
190 }
191 },
192 ValidationRule::Enum { values } => {
193 if values.contains(&value.to_string()) {
194 Ok(())
195 } else {
196 Err(FraiseQLError::Validation {
197 message: format!(
198 "Field validation failed: {}",
199 ValidationFieldError::new(
200 field_path,
201 "enum",
202 format!("Must be one of: {}", values.join(", "))
203 )
204 ),
205 path: Some(field_path.to_string()),
206 })
207 }
208 },
209 _ => Ok(()), }
211}
212
213fn extract_field_error(message: &str) -> Option<ValidationFieldError> {
215 if message.contains("Field validation failed:") {
217 if let Some(field_start) = message.find("Field validation failed: ") {
218 let rest = &message[field_start + "Field validation failed: ".len()..];
219 if let Some(paren_start) = rest.find('(') {
220 if let Some(paren_end) = rest.find(')') {
221 let field = rest[..paren_start].trim().to_string();
222 let rule_type = rest[paren_start + 1..paren_end].to_string();
223 let msg_start = rest.find(": ").unwrap_or(0) + 2;
224 let message_text = rest[msg_start..].to_string();
225 return Some(ValidationFieldError::new(field, rule_type, message_text));
226 }
227 }
228 }
229 }
230 None
231}
232
233#[cfg(test)]
234mod tests {
235 #![allow(clippy::unwrap_used)] use super::*;
238
239 #[test]
240 fn test_validation_error_collection() {
241 let mut errors = ValidationErrorCollection::new();
242 assert!(errors.is_empty());
243
244 errors.add_error(ValidationFieldError::new("email", "pattern", "Invalid email"));
245 assert!(!errors.is_empty());
246 assert_eq!(errors.len(), 1);
247 }
248
249 #[test]
250 fn test_validation_error_collection_to_error() {
251 let mut errors = ValidationErrorCollection::new();
252 errors.add_error(ValidationFieldError::new("email", "pattern", "Invalid email"));
253
254 let err = errors.to_error();
255 assert!(matches!(err, FraiseQLError::Validation { .. }));
256 }
257
258 #[test]
259 fn test_validate_required_field() {
260 let rule = ValidationRule::Required;
261 let result = validate_string_field("value", "field", &rule);
262 result.unwrap_or_else(|e| panic!("expected Ok for non-empty value: {e}"));
263
264 let result = validate_string_field("", "field", &rule);
265 assert!(
266 matches!(result, Err(FraiseQLError::Validation { .. })),
267 "expected Validation error for empty required field, got: {result:?}"
268 );
269 }
270
271 #[test]
272 fn test_validate_pattern() {
273 let rule = ValidationRule::Pattern {
274 pattern: "^[a-z]+$".to_string(),
275 message: None,
276 };
277
278 let result = validate_string_field("hello", "field", &rule);
279 result.unwrap_or_else(|e| panic!("expected Ok for matching pattern: {e}"));
280
281 let result = validate_string_field("Hello", "field", &rule);
282 assert!(
283 matches!(result, Err(FraiseQLError::Validation { .. })),
284 "expected Validation error for non-matching pattern, got: {result:?}"
285 );
286 }
287
288 #[test]
289 fn test_validate_length() {
290 let rule = ValidationRule::Length {
291 min: Some(3),
292 max: Some(10),
293 };
294
295 let result = validate_string_field("hello", "field", &rule);
296 result.unwrap_or_else(|e| panic!("expected Ok for in-range length: {e}"));
297
298 let result = validate_string_field("hi", "field", &rule);
299 assert!(
300 matches!(result, Err(FraiseQLError::Validation { .. })),
301 "expected Validation error for too-short string, got: {result:?}"
302 );
303
304 let result = validate_string_field("this is too long", "field", &rule);
305 assert!(
306 matches!(result, Err(FraiseQLError::Validation { .. })),
307 "expected Validation error for too-long string, got: {result:?}"
308 );
309 }
310
311 #[test]
312 fn test_validate_enum() {
313 let rule = ValidationRule::Enum {
314 values: vec!["active".to_string(), "inactive".to_string()],
315 };
316
317 let result = validate_string_field("active", "field", &rule);
318 result.unwrap_or_else(|e| panic!("expected Ok for valid enum value: {e}"));
319
320 let result = validate_string_field("unknown", "field", &rule);
321 assert!(
322 matches!(result, Err(FraiseQLError::Validation { .. })),
323 "expected Validation error for invalid enum value, got: {result:?}"
324 );
325 }
326
327 #[test]
328 fn test_validate_null_field() {
329 let rule = ValidationRule::Required;
330 let result = validate_input(&Value::Null, "field", &[rule]);
331 assert!(
332 matches!(result, Err(FraiseQLError::Validation { .. })),
333 "expected Validation error for null required field, got: {result:?}"
334 );
335 }
336
337 #[test]
338 fn test_validate_custom_scalar_library_code_valid() {
339 use crate::{
340 schema::CompiledSchema,
341 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
342 };
343
344 let schema = {
345 let mut s = CompiledSchema::new();
346 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
347
348 let mut def = CustomTypeDef::new("LibraryCode".to_string());
349 def.validation_rules = vec![ValidationRule::Pattern {
350 pattern: r"^LIB-[0-9]{4}$".to_string(),
351 message: Some("Library code must be LIB-#### format".to_string()),
352 }];
353
354 registry.register("LibraryCode".to_string(), def).unwrap();
355
356 s.custom_scalars = registry;
357 s
358 };
359
360 let value = serde_json::json!("LIB-1234");
361 let result = validate_custom_scalar_from_schema(&value, "LibraryCode", &schema);
362 result.unwrap_or_else(|e| panic!("expected Ok for valid LibraryCode: {e}"));
363 }
364
365 #[test]
366 fn test_validate_custom_scalar_library_code_invalid() {
367 use crate::{
368 schema::CompiledSchema,
369 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
370 };
371
372 let schema = {
373 let mut s = CompiledSchema::new();
374 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
375
376 let mut def = CustomTypeDef::new("LibraryCode".to_string());
377 def.validation_rules = vec![ValidationRule::Pattern {
378 pattern: r"^LIB-[0-9]{4}$".to_string(),
379 message: Some("Library code must be LIB-#### format".to_string()),
380 }];
381
382 registry.register("LibraryCode".to_string(), def).unwrap();
383
384 s.custom_scalars = registry;
385 s
386 };
387
388 let value = serde_json::json!("INVALID");
389 let result = validate_custom_scalar_from_schema(&value, "LibraryCode", &schema);
390 assert!(
391 matches!(result, Err(FraiseQLError::Validation { .. })),
392 "expected Validation error for invalid LibraryCode, got: {result:?}"
393 );
394 }
395
396 #[test]
397 fn test_validate_custom_scalar_student_id_with_length() {
398 use crate::{
399 schema::CompiledSchema,
400 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
401 };
402
403 let schema = {
404 let mut s = CompiledSchema::new();
405 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
406
407 let mut def = CustomTypeDef::new("StudentID".to_string());
408 def.validation_rules = vec![
409 ValidationRule::Pattern {
410 pattern: r"^STU-[0-9]{4}-[0-9]{3}$".to_string(),
411 message: None,
412 },
413 ValidationRule::Length {
414 min: Some(12),
415 max: Some(12),
416 },
417 ];
418
419 registry.register("StudentID".to_string(), def).unwrap();
420
421 s.custom_scalars = registry;
422 s
423 };
424
425 let value = serde_json::json!("STU-2024-001");
427 let result = validate_custom_scalar_from_schema(&value, "StudentID", &schema);
428 result.unwrap_or_else(|e| panic!("expected Ok for valid StudentID: {e}"));
429
430 let value = serde_json::json!("STUDENT-2024");
432 let result = validate_custom_scalar_from_schema(&value, "StudentID", &schema);
433 assert!(
434 matches!(result, Err(FraiseQLError::Validation { .. })),
435 "expected Validation error for invalid StudentID, got: {result:?}"
436 );
437 }
438
439 #[test]
440 fn test_validate_unknown_scalar_type_passthrough() {
441 use crate::schema::CompiledSchema;
442
443 let schema = CompiledSchema::new();
444
445 let value = serde_json::json!("any value");
447 let result = validate_custom_scalar_from_schema(&value, "UnknownType", &schema);
448 result.unwrap_or_else(|e| panic!("expected Ok for unknown scalar passthrough: {e}"));
449 }
450
451 #[test]
452 fn test_validate_custom_scalar_patient_id_passthrough() {
453 use crate::schema::CompiledSchema;
454
455 let schema = CompiledSchema::new();
457
458 let value = serde_json::json!("PAT-123456");
459 let result = validate_custom_scalar_from_schema(&value, "PatientID", &schema);
460 result
462 .unwrap_or_else(|e| panic!("expected Ok for unregistered PatientID passthrough: {e}"));
463 }
464
465 #[test]
466 fn test_validate_custom_scalar_with_elo_expression() {
467 use crate::{
468 schema::CompiledSchema,
469 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
470 };
471
472 let schema = {
473 let mut s = CompiledSchema::new();
474 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
475
476 let mut def = CustomTypeDef::new("StudentID".to_string());
477 def.elo_expression = Some("matches(value, \"^STU-[0-9]{4}-[0-9]{3}$\")".to_string());
478
479 registry.register("StudentID".to_string(), def).unwrap();
480
481 s.custom_scalars = registry;
482 s
483 };
484
485 let value = serde_json::json!("STU-2024-001");
487 let result = validate_custom_scalar_from_schema(&value, "StudentID", &schema);
488 result.unwrap_or_else(|e| panic!("expected Ok for StudentID matching ELO expression: {e}"));
489
490 let value = serde_json::json!("INVALID");
492 let result = validate_custom_scalar_from_schema(&value, "StudentID", &schema);
493 assert!(
494 matches!(result, Err(FraiseQLError::Validation { .. })),
495 "expected Validation error for StudentID not matching ELO expression, got: {result:?}"
496 );
497 }
498
499 #[test]
500 fn test_validate_custom_scalar_combined_rules_and_elo() {
501 use crate::{
502 schema::CompiledSchema,
503 validation::{CustomTypeDef, CustomTypeRegistry, CustomTypeRegistryConfig},
504 };
505
506 let schema = {
507 let mut s = CompiledSchema::new();
508 let registry = CustomTypeRegistry::new(CustomTypeRegistryConfig::default());
509
510 let mut def = CustomTypeDef::new("PatientID".to_string());
511 def.validation_rules = vec![ValidationRule::Length {
512 min: Some(10),
513 max: Some(10),
514 }];
515 def.elo_expression = Some("matches(value, \"^PAT-[0-9]{6}$\")".to_string());
516
517 registry.register("PatientID".to_string(), def).unwrap();
518
519 s.custom_scalars = registry;
520 s
521 };
522
523 let value = serde_json::json!("PAT-123456");
525 let result = validate_custom_scalar_from_schema(&value, "PatientID", &schema);
526 result.unwrap_or_else(|e| panic!("expected Ok for valid PatientID: {e}"));
527
528 let value = serde_json::json!("NOTVALID!");
530 let result = validate_custom_scalar_from_schema(&value, "PatientID", &schema);
531 assert!(
532 matches!(result, Err(FraiseQLError::Validation { .. })),
533 "expected Validation error for PatientID failing ELO expression, got: {result:?}"
534 );
535
536 let value = serde_json::json!("PAT-12345");
538 let result = validate_custom_scalar_from_schema(&value, "PatientID", &schema);
539 assert!(
540 matches!(result, Err(FraiseQLError::Validation { .. })),
541 "expected Validation error for PatientID failing length rule, got: {result:?}"
542 );
543 }
544}