use serde_json::Value;
use crate::error::{FraiseQLError, Result};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum InputObjectRule {
AnyOf {
fields: Vec<String>,
},
OneOf {
fields: Vec<String>,
},
ConditionalRequired {
if_field: String,
then_fields: Vec<String>,
},
RequiredIfAbsent {
absent_field: String,
then_fields: Vec<String>,
},
Custom {
name: String,
},
}
#[derive(Debug, Clone, Default)]
pub struct InputObjectValidationResult {
pub errors: Vec<String>,
pub error_count: usize,
}
impl InputObjectValidationResult {
#[must_use]
pub const fn new() -> Self {
Self {
errors: Vec::new(),
error_count: 0,
}
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
self.error_count += 1;
}
pub fn add_errors(&mut self, errors: Vec<String>) {
self.error_count += errors.len();
self.errors.extend(errors);
}
#[must_use]
pub const fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn into_result(self) -> Result<()> {
self.into_result_with_path("input")
}
pub fn into_result_with_path(self, path: &str) -> Result<()> {
if self.has_errors() {
Err(FraiseQLError::Validation {
message: format!("Input object validation failed: {}", self.errors.join("; ")),
path: Some(path.to_string()),
})
} else {
Ok(())
}
}
}
pub fn validate_input_object(
input: &Value,
rules: &[InputObjectRule],
object_path: Option<&str>,
) -> Result<()> {
let mut result = InputObjectValidationResult::new();
let path = object_path.unwrap_or("input");
if !matches!(input, Value::Object(_)) {
return Err(FraiseQLError::Validation {
message: "Input must be an object".to_string(),
path: Some(path.to_string()),
});
}
for rule in rules {
if let Err(FraiseQLError::Validation { message, .. }) = validate_rule(input, rule, path) {
result.add_error(message);
}
}
result.into_result_with_path(path)
}
fn validate_rule(input: &Value, rule: &InputObjectRule, path: &str) -> Result<()> {
match rule {
InputObjectRule::AnyOf { fields } => validate_any_of(input, fields, path),
InputObjectRule::OneOf { fields } => validate_one_of(input, fields, path),
InputObjectRule::ConditionalRequired {
if_field,
then_fields,
} => validate_conditional_required(input, if_field, then_fields, path),
InputObjectRule::RequiredIfAbsent {
absent_field,
then_fields,
} => validate_required_if_absent(input, absent_field, then_fields, path),
InputObjectRule::Custom { name } => Err(FraiseQLError::Validation {
message: format!(
"Custom validator '{name}' is not registered. \
Register validators via InputValidatorRegistry before executing queries."
),
path: Some(path.to_string()),
}),
}
}
fn validate_any_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
if let Value::Object(obj) = input {
let has_any = fields
.iter()
.any(|name| obj.get(name).is_some_and(|v| !matches!(v, Value::Null)));
if !has_any {
return Err(FraiseQLError::Validation {
message: format!("At least one of [{}] must be provided", fields.join(", ")),
path: Some(path.to_string()),
});
}
}
Ok(())
}
fn validate_one_of(input: &Value, fields: &[String], path: &str) -> Result<()> {
if let Value::Object(obj) = input {
let present_count = fields
.iter()
.filter(|name| obj.get(*name).is_some_and(|v| !matches!(v, Value::Null)))
.count();
if present_count != 1 {
return Err(FraiseQLError::Validation {
message: format!(
"Exactly one of [{}] must be provided, but {} {} provided",
fields.join(", "),
present_count,
if present_count == 1 { "was" } else { "were" }
),
path: Some(path.to_string()),
});
}
}
Ok(())
}
fn validate_conditional_required(
input: &Value,
if_field: &str,
then_fields: &[String],
path: &str,
) -> Result<()> {
if let Value::Object(obj) = input {
let condition_met = obj.get(if_field).is_some_and(|v| !matches!(v, Value::Null));
if condition_met {
let missing_fields: Vec<&String> = then_fields
.iter()
.filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
.collect();
if !missing_fields.is_empty() {
return Err(FraiseQLError::Validation {
message: format!(
"Since '{}' is provided, {} must also be provided",
if_field,
missing_fields
.iter()
.map(|s| format!("'{}'", s))
.collect::<Vec<_>>()
.join(", ")
),
path: Some(path.to_string()),
});
}
}
}
Ok(())
}
fn validate_required_if_absent(
input: &Value,
absent_field: &str,
then_fields: &[String],
path: &str,
) -> Result<()> {
if let Value::Object(obj) = input {
let field_absent = obj.get(absent_field).is_none_or(|v| matches!(v, Value::Null));
if field_absent {
let missing_fields: Vec<&String> = then_fields
.iter()
.filter(|name| obj.get(*name).is_none_or(|v| matches!(v, Value::Null)))
.collect();
if !missing_fields.is_empty() {
return Err(FraiseQLError::Validation {
message: format!(
"Since '{}' is not provided, {} must be provided",
absent_field,
missing_fields
.iter()
.map(|s| format!("'{}'", s))
.collect::<Vec<_>>()
.join(", ")
),
path: Some(path.to_string()),
});
}
}
}
Ok(())
}