fraiseql_core/runtime/
input_validator.rs1use serde_json::Value;
8
9use crate::{
10 error::{FraiseQLError, Result, ValidationFieldError},
11 schema::CompiledSchema,
12 validation::{LengthCheck, ValidationRule, check_length},
13};
14
15#[derive(Debug, Clone, Default)]
17pub struct ValidationErrorCollection {
18 pub errors: Vec<ValidationFieldError>,
20}
21
22impl ValidationErrorCollection {
23 #[must_use]
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 pub fn add_error(&mut self, error: ValidationFieldError) {
31 self.errors.push(error);
32 }
33
34 #[must_use]
36 pub const fn is_empty(&self) -> bool {
37 self.errors.is_empty()
38 }
39
40 #[must_use]
42 pub const fn len(&self) -> usize {
43 self.errors.len()
44 }
45
46 #[must_use]
48 pub fn to_error(&self) -> FraiseQLError {
49 if self.errors.is_empty() {
50 FraiseQLError::validation("No validation errors")
51 } else if self.errors.len() == 1 {
52 let err = &self.errors[0];
53 FraiseQLError::Validation {
54 message: err.to_string(),
55 path: Some(err.field.clone()),
56 }
57 } else {
58 let messages: Vec<String> = self.errors.iter().map(|e| e.to_string()).collect();
59 FraiseQLError::Validation {
60 message: format!("Multiple validation errors: {}", messages.join("; ")),
61 path: None,
62 }
63 }
64 }
65}
66
67pub fn validate_custom_scalar_from_schema(
82 value: &Value,
83 scalar_type_name: &str,
84 schema: &CompiledSchema,
85) -> Result<()> {
86 if schema.custom_scalars.exists(scalar_type_name) {
88 schema.custom_scalars.validate(scalar_type_name, value)
89 } else {
90 Ok(())
92 }
93}
94
95pub fn validate_input(value: &Value, field_path: &str, rules: &[ValidationRule]) -> Result<()> {
105 let mut errors = ValidationErrorCollection::new();
106
107 match value {
108 Value::String(s) => {
109 for rule in rules {
110 if let Err(field_err) = check_string_field(s, field_path, rule) {
116 errors.add_error(field_err);
117 }
118 }
119 },
120 Value::Null => {
121 for rule in rules {
122 if rule.is_required() {
123 errors.add_error(ValidationFieldError::new(
124 field_path,
125 "required",
126 "Field is required",
127 ));
128 }
129 }
130 },
131 _ => {
132 },
134 }
135
136 if errors.is_empty() {
137 Ok(())
138 } else {
139 Err(errors.to_error())
140 }
141}
142
143pub(crate) fn check_string_field(
149 value: &str,
150 field_path: &str,
151 rule: &ValidationRule,
152) -> std::result::Result<(), ValidationFieldError> {
153 match rule {
154 ValidationRule::Required if value.is_empty() => {
155 Err(ValidationFieldError::new(field_path, "required", "Field is required"))
156 },
157 ValidationRule::Pattern { pattern, message } if !pattern.is_match(value) => {
158 let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
159 Err(ValidationFieldError::new(field_path, "pattern", msg))
160 },
161 ValidationRule::Length { min, max } => match check_length(value, *min, *max) {
162 LengthCheck::Ok => Ok(()),
163 LengthCheck::TooShort { min, actual } => Err(ValidationFieldError::new(
164 field_path,
165 "length",
166 format!("Length must be at least {min} characters, got {actual}"),
167 )),
168 LengthCheck::TooLong { max, actual } => Err(ValidationFieldError::new(
169 field_path,
170 "length",
171 format!("Length must be at most {max} characters, got {actual}"),
172 )),
173 },
174 ValidationRule::Enum { values } if !values.iter().any(|v| v == value) => {
175 Err(ValidationFieldError::new(
176 field_path,
177 "enum",
178 format!("Must be one of: {}", values.join(", ")),
179 ))
180 },
181 _ => Ok(()),
183 }
184}