use crate::{
error::{FraiseQLError, Result},
graphql::types::FieldSelection,
schema::CompiledSchema,
};
const MAX_VALIDATION_DEPTH: usize = 16;
pub fn validate_selection_set(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
) -> Result<()> {
validate_at(schema, type_name, selections, 0)
}
fn composite_type_of(field_type: &crate::schema::FieldType) -> Option<&str> {
use crate::schema::FieldType;
match field_type.inner_type().unwrap_or(field_type) {
FieldType::Object(name) | FieldType::Interface(name) | FieldType::Union(name) => Some(name),
_ => None,
}
}
fn requires_selection_set(schema: &CompiledSchema, type_name: &str) -> bool {
if let Some(type_def) = schema.find_type(type_name) {
return !type_def.fields.is_empty();
}
schema.find_union(type_name).is_some()
}
fn root_needs_selection_set(type_name: &str) -> FraiseQLError {
FraiseQLError::Validation {
message: format!("Field of composite type '{type_name}' must have a selection set."),
path: Some(type_name.to_string()),
}
}
fn field_needs_selection_set(parent: &str, field: &str, child_type: &str) -> FraiseQLError {
FraiseQLError::Validation {
message: format!(
"Field '{field}' of composite type '{child_type}' must have a selection set."
),
path: Some(format!("{parent}.{field}")),
}
}
pub fn validate_leaf_field_selections(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
) -> Result<()> {
leaf_check_at(schema, type_name, selections, 0)
}
fn leaf_check_at(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
depth: usize,
) -> Result<()> {
if depth >= MAX_VALIDATION_DEPTH {
return Ok(());
}
if selections.is_empty() {
return if requires_selection_set(schema, type_name) {
Err(root_needs_selection_set(type_name))
} else {
Ok(())
};
}
let Some(type_def) = schema.find_type(type_name) else {
for sel in selections {
if let Some(condition) = sel.name.strip_prefix("...on ") {
let condition = condition.trim();
if schema.find_type(condition).is_some() {
leaf_check_at(schema, condition, &sel.nested_fields, depth + 1)?;
}
}
}
return Ok(());
};
if type_def.fields.is_empty() {
return Ok(());
}
for sel in selections {
if let Some(condition) = sel.name.strip_prefix("...on ") {
let condition = condition.trim();
if schema.find_type(condition).is_some() {
leaf_check_at(schema, condition, &sel.nested_fields, depth + 1)?;
}
continue;
}
if sel.name.starts_with("...") || sel.name.starts_with("__") {
continue;
}
let Some(field_def) = type_def.fields.iter().find(|f| f.name == sel.name) else {
continue;
};
let Some(child_type) = composite_type_of(&field_def.field_type) else {
continue;
};
if sel.nested_fields.is_empty() {
if requires_selection_set(schema, child_type) {
return Err(field_needs_selection_set(type_name, &sel.name, child_type));
}
continue;
}
leaf_check_at(schema, child_type, &sel.nested_fields, depth + 1)?;
}
Ok(())
}
fn validate_at(
schema: &CompiledSchema,
type_name: &str,
selections: &[FieldSelection],
depth: usize,
) -> Result<()> {
if depth >= MAX_VALIDATION_DEPTH {
return Ok(());
}
if schema.find_type(type_name).is_none() && schema.find_union(type_name).is_some() {
for sel in selections {
if let Some(condition) = sel.name.strip_prefix("...on ") {
let condition = condition.trim();
if schema.find_type(condition).is_some() {
validate_at(schema, condition, &sel.nested_fields, depth + 1)?;
}
}
}
return Ok(());
}
let Some(type_def) = schema.find_type(type_name) else {
return Ok(());
};
if type_def.fields.is_empty() {
return Ok(());
}
for sel in selections {
if let Some(condition) = sel.name.strip_prefix("...on ") {
let condition = condition.trim();
if schema.find_type(condition).is_some() {
validate_at(schema, condition, &sel.nested_fields, depth + 1)?;
}
continue;
}
if sel.name.starts_with("...") {
continue;
}
if sel.name.starts_with("__") {
continue;
}
let Some(field_def) = type_def.fields.iter().find(|f| f.name == sel.name) else {
return Err(FraiseQLError::Validation {
message: format!("Cannot query field '{}' on type '{type_name}'.", sel.name),
path: Some(format!("{type_name}.{}", sel.name)),
});
};
if sel.nested_fields.is_empty() {
continue;
}
if let Some(child_type) = composite_type_of(&field_def.field_type) {
validate_at(schema, child_type, &sel.nested_fields, depth + 1)?;
}
}
Ok(())
}
#[cfg(test)]
#[path = "selection_validation_tests.rs"]
mod selection_validation_tests;