fraiseql_core/validation/
input_object.rs1use serde_json::Value;
31
32use crate::error::{FraiseQLError, Result};
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub enum InputObjectRule {
38 AnyOf {
40 fields: Vec<String>,
42 },
43 OneOf {
45 fields: Vec<String>,
47 },
48 ConditionalRequired {
50 if_field: String,
52 then_fields: Vec<String>,
54 },
55 RequiredIfAbsent {
57 absent_field: String,
59 then_fields: Vec<String>,
61 },
62 Custom {
64 name: String,
66 },
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct InputObjectValidationResult {
72 pub errors: Vec<String>,
74 pub error_count: usize,
76}
77
78impl InputObjectValidationResult {
79 #[must_use]
81 pub const fn new() -> Self {
82 Self {
83 errors: Vec::new(),
84 error_count: 0,
85 }
86 }
87
88 pub fn add_error(&mut self, error: String) {
90 self.errors.push(error);
91 self.error_count += 1;
92 }
93
94 pub fn add_errors(&mut self, errors: Vec<String>) {
96 self.error_count += errors.len();
97 self.errors.extend(errors);
98 }
99
100 #[must_use]
102 pub const fn has_errors(&self) -> bool {
103 !self.errors.is_empty()
104 }
105
106 pub fn into_result(self) -> Result<()> {
113 self.into_result_with_path("input")
114 }
115
116 pub fn into_result_with_path(self, path: &str) -> Result<()> {
123 if self.has_errors() {
124 Err(FraiseQLError::Validation {
125 message: format!("Input object validation failed: {}", self.errors.join("; ")),
126 path: Some(path.to_string()),
127 })
128 } else {
129 Ok(())
130 }
131 }
132}
133
134pub fn validate_input_object(
147 input: &Value,
148 rules: &[InputObjectRule],
149 object_path: Option<&str>,
150) -> Result<()> {
151 let mut result = InputObjectValidationResult::new();
152 let path = object_path.unwrap_or("input");
153
154 if !matches!(input, Value::Object(_)) {
155 return Err(FraiseQLError::Validation {
156 message: "Input must be an object".to_string(),
157 path: Some(path.to_string()),
158 });
159 }
160
161 for rule in rules {
162 if let Err(FraiseQLError::Validation { message, .. }) = validate_rule(input, rule, path) {
163 result.add_error(message);
164 }
165 }
166
167 result.into_result_with_path(path)
168}
169
170fn validate_rule(input: &Value, rule: &InputObjectRule, path: &str) -> Result<()> {
172 match rule {
173 InputObjectRule::AnyOf { fields } => validate_any_of(input, fields, path),
174 InputObjectRule::OneOf { fields } => validate_one_of(input, fields, path),
175 InputObjectRule::ConditionalRequired {
176 if_field,
177 then_fields,
178 } => validate_conditional_required(input, if_field, then_fields, path),
179 InputObjectRule::RequiredIfAbsent {
180 absent_field,
181 then_fields,
182 } => validate_required_if_absent(input, absent_field, then_fields, path),
183 InputObjectRule::Custom { name } => Err(FraiseQLError::Validation {
184 message: format!(
185 "Custom validator '{name}' is not registered. \
186 Register validators via InputValidatorRegistry before executing queries."
187 ),
188 path: Some(path.to_string()),
189 }),
190 }
191}
192
193fn validate_any_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
195 if let Value::Object(obj) = input {
196 let has_any = fields
197 .iter()
198 .any(|name| obj.get(name).is_some_and(|v| !matches!(v, Value::Null)));
199
200 if !has_any {
201 return Err(FraiseQLError::Validation {
202 message: format!("At least one of [{}] must be provided", fields.join(", ")),
203 path: Some(path.to_string()),
204 });
205 }
206 }
207
208 Ok(())
209}
210
211fn validate_one_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
213 if let Value::Object(obj) = input {
214 let present_count = fields
215 .iter()
216 .filter(|name| obj.get(*name).is_some_and(|v| !matches!(v, Value::Null)))
217 .count();
218
219 if present_count != 1 {
220 return Err(FraiseQLError::Validation {
221 message: format!(
222 "Exactly one of [{}] must be provided, but {} {} provided",
223 fields.join(", "),
224 present_count,
225 if present_count == 1 { "was" } else { "were" }
226 ),
227 path: Some(path.to_string()),
228 });
229 }
230 }
231
232 Ok(())
233}
234
235fn validate_conditional_required(
237 input: &Value,
238 if_field: &str,
239 then_fields: &[String],
240 path: &str,
241) -> Result<()> {
242 if let Value::Object(obj) = input {
243 let condition_met = obj.get(if_field).is_some_and(|v| !matches!(v, Value::Null));
244
245 if condition_met {
246 let missing_fields: Vec<&String> = then_fields
247 .iter()
248 .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
249 .collect();
250
251 if !missing_fields.is_empty() {
252 return Err(FraiseQLError::Validation {
253 message: format!(
254 "Since '{}' is provided, {} must also be provided",
255 if_field,
256 missing_fields
257 .iter()
258 .map(|s| format!("'{}'", s))
259 .collect::<Vec<_>>()
260 .join(", ")
261 ),
262 path: Some(path.to_string()),
263 });
264 }
265 }
266 }
267
268 Ok(())
269}
270
271fn validate_required_if_absent(
273 input: &Value,
274 absent_field: &str,
275 then_fields: &[String],
276 path: &str,
277) -> Result<()> {
278 if let Value::Object(obj) = input {
279 let field_absent = obj.get(absent_field).is_none_or(|v| matches!(v, Value::Null));
280
281 if field_absent {
282 let missing_fields: Vec<&String> = then_fields
283 .iter()
284 .filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
285 .collect();
286
287 if !missing_fields.is_empty() {
288 return Err(FraiseQLError::Validation {
289 message: format!(
290 "Since '{}' is not provided, {} must be provided",
291 absent_field,
292 missing_fields
293 .iter()
294 .map(|s| format!("'{}'", s))
295 .collect::<Vec<_>>()
296 .join(", ")
297 ),
298 path: Some(path.to_string()),
299 });
300 }
301 }
302 }
303
304 Ok(())
305}