fraiseql_core/runtime/
input_validator.rs1use 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 #[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(FraiseQLError::Validation { message, .. }) =
111 validate_string_field(s, field_path, rule)
112 {
113 if let Some(field_err) = extract_field_error(&message) {
114 errors.add_error(field_err);
115 }
116 }
117 }
118 },
119 Value::Null => {
120 for rule in rules {
121 if rule.is_required() {
122 errors.add_error(ValidationFieldError::new(
123 field_path,
124 "required",
125 "Field is required",
126 ));
127 }
128 }
129 },
130 _ => {
131 },
133 }
134
135 if errors.is_empty() {
136 Ok(())
137 } else {
138 Err(errors.to_error())
139 }
140}
141
142pub(crate) fn validate_string_field(
144 value: &str,
145 field_path: &str,
146 rule: &ValidationRule,
147) -> Result<()> {
148 match rule {
149 ValidationRule::Required => {
150 if value.is_empty() {
151 return Err(FraiseQLError::Validation {
152 message: format!(
153 "Field validation failed: {}",
154 ValidationFieldError::new(field_path, "required", "Field is required")
155 ),
156 path: Some(field_path.to_string()),
157 });
158 }
159 Ok(())
160 },
161 ValidationRule::Pattern { pattern, message } => {
162 if pattern.is_match(value) {
163 Ok(())
164 } else {
165 let msg = message.clone().unwrap_or_else(|| "Pattern mismatch".to_string());
166 Err(FraiseQLError::Validation {
167 message: format!(
168 "Field validation failed: {}",
169 ValidationFieldError::new(field_path, "pattern", msg)
170 ),
171 path: Some(field_path.to_string()),
172 })
173 }
174 },
175 ValidationRule::Length { min, max } => {
176 let len = value.len();
177 let valid = if let Some(m) = min { len >= *m } else { true }
178 && if let Some(m) = max { len <= *m } else { true };
179
180 if valid {
181 Ok(())
182 } else {
183 let msg = match (min, max) {
184 (Some(m), Some(x)) => format!("Length must be between {} and {}", m, x),
185 (Some(m), None) => format!("Length must be at least {}", m),
186 (None, Some(x)) => format!("Length must be at most {}", x),
187 (None, None) => "Length validation failed".to_string(),
188 };
189 Err(FraiseQLError::Validation {
190 message: format!(
191 "Field validation failed: {}",
192 ValidationFieldError::new(field_path, "length", msg)
193 ),
194 path: Some(field_path.to_string()),
195 })
196 }
197 },
198 ValidationRule::Enum { values } => {
199 if values.contains(&value.to_string()) {
200 Ok(())
201 } else {
202 Err(FraiseQLError::Validation {
203 message: format!(
204 "Field validation failed: {}",
205 ValidationFieldError::new(
206 field_path,
207 "enum",
208 format!("Must be one of: {}", values.join(", "))
209 )
210 ),
211 path: Some(field_path.to_string()),
212 })
213 }
214 },
215 _ => Ok(()), }
217}
218
219fn extract_field_error(message: &str) -> Option<ValidationFieldError> {
221 if message.contains("Field validation failed:") {
223 if let Some(field_start) = message.find("Field validation failed: ") {
224 let rest = &message[field_start + "Field validation failed: ".len()..];
225 if let Some(paren_start) = rest.find('(') {
226 if let Some(paren_end) = rest.find(')') {
227 let field = rest[..paren_start].trim().to_string();
228 let rule_type = rest[paren_start + 1..paren_end].to_string();
229 let msg_start = rest.find(": ").unwrap_or(0) + 2;
230 let message_text = rest[msg_start..].to_string();
231 return Some(ValidationFieldError::new(field, rule_type, message_text));
232 }
233 }
234 }
235 }
236 None
237}