use super::ast::{
self, Document, Field, Filter, FragmentDef, Mutation, Query, Selection, Subscription, Value,
};
use crate::types::{Collection, FieldType, ScalarType};
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
UnknownCollection,
UnknownField,
InvalidInput,
TypeMismatch,
MissingRequiredVariable,
MissingOptionalVariable,
InvalidFilterOperator,
DuplicateAlias,
InvalidArgument,
UnknownDirective,
UnknownFragment,
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorCode::UnknownCollection => write!(f, "UnknownCollection"),
ErrorCode::UnknownField => write!(f, "UnknownField"),
ErrorCode::InvalidInput => write!(f, "InvalidInput"),
ErrorCode::TypeMismatch => write!(f, "TypeMismatch"),
ErrorCode::MissingRequiredVariable => write!(f, "MissingRequiredVariable"),
ErrorCode::MissingOptionalVariable => write!(f, "MissingOptionalVariable"),
ErrorCode::InvalidFilterOperator => write!(f, "InvalidFilterOperator"),
ErrorCode::DuplicateAlias => write!(f, "DuplicateAlias"),
ErrorCode::InvalidArgument => write!(f, "InvalidArgument"),
ErrorCode::UnknownDirective => write!(f, "UnknownDirective"),
ErrorCode::UnknownFragment => write!(f, "UnknownFragment"),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub code: ErrorCode,
pub message: String,
pub path: Option<String>,
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.path {
Some(path) => write!(f, "[{}] {}", path, self.message),
None => write!(f, "{}", self.message),
}
}
}
impl std::error::Error for ValidationError {}
impl ValidationError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
path: None,
}
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
}
pub type ValidationResult = Result<(), Vec<ValidationError>>;
pub trait SchemaProvider {
fn get_collection(&self, name: &str) -> Option<&Collection>;
fn collection_exists(&self, name: &str) -> bool {
self.get_collection(name).is_some()
}
}
#[derive(Debug, Default)]
pub struct InMemorySchema {
collections: HashMap<String, Collection>,
}
impl InMemorySchema {
pub fn new() -> Self {
Self::default()
}
pub fn add_collection(&mut self, collection: Collection) {
self.collections.insert(collection.name.clone(), collection);
}
}
impl SchemaProvider for InMemorySchema {
fn get_collection(&self, name: &str) -> Option<&Collection> {
self.collections.get(name)
}
}
pub struct ValidationContext<'a, S: SchemaProvider> {
pub schema: &'a S,
pub variables: HashMap<String, ast::Value>,
pub fragments: HashMap<String, &'a FragmentDef>,
pub errors: Vec<ValidationError>,
current_path: Vec<String>,
validating_fragments: HashSet<String>,
}
impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
pub fn new(schema: &'a S) -> Self {
Self {
schema,
variables: HashMap::new(),
fragments: HashMap::new(),
errors: Vec::new(),
current_path: Vec::new(),
validating_fragments: HashSet::new(),
}
}
pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
self.variables = variables;
self
}
fn push_path(&mut self, segment: &str) {
self.current_path.push(segment.to_string());
}
fn pop_path(&mut self) {
self.current_path.pop();
}
fn current_path_string(&self) -> Option<String> {
if self.current_path.is_empty() {
None
} else {
Some(self.current_path.join("."))
}
}
fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
let mut error = ValidationError::new(code, message);
error.path = self.current_path_string();
self.errors.push(error);
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn into_result(self) -> ValidationResult {
if self.errors.is_empty() {
Ok(())
} else {
Err(self.errors)
}
}
}
pub fn validate_document<S: SchemaProvider>(
doc: &Document,
schema: &S,
variables: HashMap<String, ast::Value>,
) -> ValidationResult {
let mut ctx = ValidationContext::new(schema).with_variables(variables);
for op in &doc.operations {
if let ast::Operation::FragmentDefinition(frag) = op {
if ctx.fragments.insert(frag.name.clone(), frag).is_some() {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Duplicate fragment definition '{}'", frag.name),
);
}
}
}
for (i, op) in doc.operations.iter().enumerate() {
ctx.push_path(&format!("operation[{}]", i));
match op {
ast::Operation::Query(query) => validate_query(query, &mut ctx),
ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
ast::Operation::Schema(_) => {} ast::Operation::Migration(_) => {}
ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
ctx.pop_path();
}
ctx.into_result()
}
fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
if let Some(name) = &query.name {
ctx.push_path(name);
}
validate_variable_definitions(&query.variable_definitions, ctx);
for selection in &query.selection_set {
match selection {
Selection::Field(field) => {
validate_field(field, None, ctx);
}
Selection::InlineFragment(inline) => {
validate_inline_fragment(inline, None, ctx);
}
Selection::FragmentSpread(fragment_name) => {
ctx.add_error(
ErrorCode::InvalidArgument,
format!(
"Fragment spread '{}' is not allowed at query root; \
use it inside a collection selection set instead",
fragment_name
),
);
}
}
}
if query.name.is_some() {
ctx.pop_path();
}
}
fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
if let Some(name) = &mutation.name {
ctx.push_path(name);
}
validate_variable_definitions(&mutation.variable_definitions, ctx);
for (i, op) in mutation.operations.iter().enumerate() {
ctx.push_path(&format!("mutation[{}]", i));
validate_mutation_operation(op, ctx);
ctx.pop_path();
}
if mutation.name.is_some() {
ctx.pop_path();
}
}
fn validate_inline_fragment<S: SchemaProvider>(
inline: &ast::InlineFragment,
expected_collection: Option<&str>,
ctx: &mut ValidationContext<'_, S>,
) {
if let Some(enc) = expected_collection {
if inline.type_condition != enc {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Inline fragment on '{}' cannot appear inside '{}'",
inline.type_condition, enc
),
);
return;
}
}
if !ctx.schema.collection_exists(&inline.type_condition) {
ctx.add_error(
ErrorCode::UnknownCollection,
format!(
"Unknown collection '{}' in inline fragment",
inline.type_condition
),
);
return;
}
if let Some(collection) = ctx.schema.get_collection(&inline.type_condition) {
validate_selection_set(&inline.selection_set, collection, ctx);
}
}
fn validate_subscription<S: SchemaProvider>(
sub: &Subscription,
ctx: &mut ValidationContext<'_, S>,
) {
if let Some(name) = &sub.name {
ctx.push_path(name);
}
validate_variable_definitions(&sub.variable_definitions, ctx);
for selection in &sub.selection_set {
match selection {
Selection::Field(field) => {
validate_field(field, None, ctx);
}
Selection::InlineFragment(inline) => {
validate_inline_fragment(inline, None, ctx);
}
Selection::FragmentSpread(fragment_name) => {
if let Some(fragment) = ctx.fragments.get(fragment_name) {
validate_fragment_spread(fragment_name, &fragment.type_condition, ctx);
} else {
ctx.add_error(
ErrorCode::UnknownFragment,
format!("Fragment '{}' is not defined", fragment_name),
);
}
}
}
}
if sub.name.is_some() {
ctx.pop_path();
}
}
fn validate_variable_definitions<S: SchemaProvider>(
definitions: &[ast::VariableDefinition],
ctx: &mut ValidationContext<'_, S>,
) {
for def in definitions {
let var_name = &def.name;
if def.var_type.is_required && def.default_value.is_none() {
if !ctx.variables.contains_key(var_name) {
ctx.add_error(
ErrorCode::MissingRequiredVariable,
format!("Required variable '{}' is not provided", var_name),
);
}
}
}
}
fn validate_field<S: SchemaProvider>(
field: &Field,
parent_collection: Option<&Collection>,
ctx: &mut ValidationContext<'_, S>,
) {
let field_name = field.alias.as_ref().unwrap_or(&field.name);
ctx.push_path(field_name);
match parent_collection {
None => {
let collection_name = &field.name;
if !ctx.schema.collection_exists(collection_name) {
ctx.add_error(
ErrorCode::UnknownCollection,
format!("Collection '{}' does not exist", collection_name),
);
} else if field.selection_set.is_empty() {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Collection '{}' requires a selection set", collection_name),
);
} else if let Some(collection) = ctx.schema.get_collection(collection_name) {
validate_selection_set(&field.selection_set, collection, ctx);
for arg in &field.arguments {
if arg.name == "where" || arg.name == "filter" {
report_unknown_filter_ops(&arg.value, ctx);
if let Some(filter) = extract_filter_from_value(&arg.value) {
validate_filter(&filter, collection, ctx);
}
}
}
}
}
Some(collection) => {
if !field.selection_set.is_empty() {
if let Some(field_def) = collection.fields.get(&field.name) {
match &field_def.field_type {
FieldType::Nested(nested_schema) => {
validate_nested_selection_set(&field.selection_set, nested_schema, ctx);
}
FieldType::Object | FieldType::Any => {
}
FieldType::Scalar(ScalarType::Object)
| FieldType::Scalar(ScalarType::Any) => {
}
_ => {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Field '{}' is not an object type but has a selection set",
field.name
),
);
}
}
}
}
}
}
ctx.pop_path();
}
fn validate_selection_set<S: SchemaProvider>(
selections: &[Selection],
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
let mut aliases_seen: HashMap<String, bool> = HashMap::new();
for selection in selections {
match selection {
Selection::Field(field) => {
let display_name = field.alias.as_ref().unwrap_or(&field.name);
if aliases_seen.contains_key(display_name) {
ctx.add_error(
ErrorCode::DuplicateAlias,
format!("Duplicate field/alias '{}' in selection", display_name),
);
}
aliases_seen.insert(display_name.clone(), true);
let field_name = &field.name;
if !field_name.starts_with("__") && field_name != "id" {
if !collection.fields.contains_key(field_name) {
ctx.add_error(
ErrorCode::UnknownField,
format!(
"Field '{}' does not exist in collection '{}'",
field_name, collection.name
),
);
}
}
validate_field(field, Some(collection), ctx);
}
Selection::InlineFragment(inline) => {
validate_inline_fragment(inline, Some(&collection.name), ctx);
}
Selection::FragmentSpread(fragment_name) => {
validate_fragment_spread(fragment_name, &collection.name, ctx);
}
}
}
}
fn validate_nested_selection_set<S: SchemaProvider>(
selections: &[Selection],
nested_schema: &HashMap<String, crate::types::FieldDefinition>,
ctx: &mut ValidationContext<'_, S>,
) {
let mut aliases_seen: HashMap<String, bool> = HashMap::new();
for selection in selections {
match selection {
Selection::Field(field) => {
let display_name = field.alias.as_ref().unwrap_or(&field.name);
if aliases_seen.contains_key(display_name) {
ctx.add_error(
ErrorCode::DuplicateAlias,
format!("Duplicate field/alias '{}' in selection", display_name),
);
}
aliases_seen.insert(display_name.clone(), true);
let field_name = &field.name;
if !field_name.starts_with("__") {
if !nested_schema.contains_key(field_name) {
ctx.add_error(
ErrorCode::UnknownField,
format!("Field '{}' does not exist in nested object", field_name),
);
} else if !field.selection_set.is_empty() {
if let Some(field_def) = nested_schema.get(field_name) {
match &field_def.field_type {
FieldType::Nested(deeper_schema) => {
ctx.push_path(field_name);
validate_nested_selection_set(
&field.selection_set,
deeper_schema,
ctx,
);
ctx.pop_path();
}
FieldType::Object | FieldType::Any => {
}
FieldType::Scalar(ScalarType::Object)
| FieldType::Scalar(ScalarType::Any) => {
}
_ => {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Field '{}' is not an object type but has a selection set",
field_name
),
);
}
}
}
}
}
}
Selection::InlineFragment(_) | Selection::FragmentSpread(_) => {
ctx.add_error(
ErrorCode::InvalidInput,
"Fragments are not supported in nested object selections".to_string(),
);
}
}
}
}
fn validate_fragment_spread<S: SchemaProvider>(
fragment_name: &str,
expected_collection: &str,
ctx: &mut ValidationContext<'_, S>,
) {
if ctx.validating_fragments.contains(fragment_name) {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Fragment '{}' contains a cyclic reference", fragment_name),
);
return;
}
let fragment_opt = ctx.fragments.get(fragment_name).cloned();
if let Some(fragment) = fragment_opt {
if fragment.type_condition != expected_collection {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Fragment '{}' is defined on '{}' but used on '{}'",
fragment_name, fragment.type_condition, expected_collection
),
);
return;
}
if let Some(collection) = ctx.schema.get_collection(expected_collection) {
ctx.push_path(&format!("...{}", fragment_name));
ctx.validating_fragments.insert(fragment_name.to_string());
validate_selection_set(&fragment.selection_set, collection, ctx);
ctx.validating_fragments.remove(fragment_name);
ctx.pop_path();
} else {
ctx.add_error(
ErrorCode::UnknownCollection,
format!(
"Fragment '{}' references unknown collection '{}'",
fragment_name, expected_collection
),
);
}
} else {
ctx.add_error(
ErrorCode::UnknownFragment,
format!("Fragment '{}' is not defined", fragment_name),
);
}
}
fn validate_mutation_operation<S: SchemaProvider>(
op: &ast::MutationOperation,
ctx: &mut ValidationContext<'_, S>,
) {
match &op.operation {
ast::MutationOp::Insert { collection, data } => {
if let Some(col_def) = ctx.schema.get_collection(collection) {
validate_object_against_schema(data, col_def, ctx);
} else {
ctx.add_error(
ErrorCode::UnknownCollection,
format!("Collection '{}' does not exist", collection),
);
}
}
ast::MutationOp::InsertMany { collection, data } => {
if let Some(col_def) = ctx.schema.get_collection(collection) {
for item in data {
validate_object_against_schema(item, col_def, ctx);
}
} else {
ctx.add_error(
ErrorCode::UnknownCollection,
format!("Collection '{}' does not exist", collection),
);
}
}
ast::MutationOp::Update {
collection, data, ..
}
| ast::MutationOp::Upsert {
collection, data, ..
} => {
if let Some(col_def) = ctx.schema.get_collection(collection) {
validate_partial_object(data, col_def, ctx);
} else {
ctx.add_error(
ErrorCode::UnknownCollection,
format!("Collection '{}' does not exist", collection),
);
}
}
ast::MutationOp::Delete { collection, .. } => {
if !ctx.schema.collection_exists(collection) {
ctx.add_error(
ErrorCode::UnknownCollection,
format!("Collection '{}' does not exist", collection),
);
}
}
ast::MutationOp::EnqueueJob { .. } => {}
ast::MutationOp::EnqueueJobs { .. } => {}
ast::MutationOp::Import { collection, data } => {
if let Some(col_def) = ctx.schema.get_collection(collection) {
for item in data {
validate_object_against_schema(item, col_def, ctx);
}
}
}
ast::MutationOp::Export { .. } => {}
ast::MutationOp::Transaction { operations } => {
for (i, inner_op) in operations.iter().enumerate() {
ctx.push_path(&format!("tx[{}]", i));
validate_mutation_operation(inner_op, ctx);
ctx.pop_path();
}
}
}
}
fn validate_object_against_schema<S: SchemaProvider>(
value: &Value,
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
match value {
Value::Object(map) => {
for (key, val) in map {
if let Some(field_def) = collection.fields.get(key) {
if matches!(val, Value::Null) && !field_def.nullable {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Field '{}' cannot be null", key),
);
} else if !matches!(val, Value::Null)
&& !validate_value_against_type(val, &field_def.field_type)
{
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Type mismatch for field '{}': expected {:?}, got {:?}",
key,
field_def.field_type,
value_type_name(val)
),
);
}
} else if key != "id" {
ctx.add_error(
ErrorCode::UnknownField,
format!(
"Field '{}' not defined in collection '{}'",
key, collection.name
),
);
}
}
for (name, def) in &collection.fields {
if !def.nullable && !map.contains_key(name) {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Missing required field '{}'", name),
);
}
}
}
_ => {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Expected object for collection '{}', got {:?}",
collection.name,
value_type_name(value)
),
);
}
}
}
fn validate_partial_object<S: SchemaProvider>(
value: &Value,
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
match value {
Value::Object(map) => {
for (key, val) in map {
if let Some(field_def) = collection.fields.get(key) {
if matches!(val, Value::Null) && !field_def.nullable {
ctx.add_error(
ErrorCode::InvalidInput,
format!("Field '{}' cannot be null", key),
);
} else if !matches!(val, Value::Null)
&& !validate_value_against_type(val, &field_def.field_type)
{
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Type mismatch for field '{}': expected {:?}, got {:?}",
key,
field_def.field_type,
value_type_name(val)
),
);
}
} else if key != "id" {
ctx.add_error(
ErrorCode::UnknownField,
format!(
"Field '{}' not defined in collection '{}'",
key, collection.name
),
);
}
}
}
_ => {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Expected object for collection '{}', got {:?}",
collection.name,
value_type_name(value)
),
);
}
}
}
fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
use crate::types::ScalarType;
match (expected, value) {
(FieldType::Scalar(ScalarType::Any), _) => true,
(_, Value::Null) => true,
(_, Value::Variable(_)) => true,
(FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
(FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
(FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
(FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
(FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
(FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(),
(FieldType::Array(scalar_inner), Value::Array(items)) => {
let inner_field_type = FieldType::Scalar(scalar_inner.clone());
items
.iter()
.all(|item| validate_value_against_type(item, &inner_field_type))
}
(FieldType::Object, Value::Object(_)) => true,
(FieldType::Nested(schema), Value::Object(map)) => {
for (key, val) in map {
if let Some(def) = schema.get(key) {
if matches!(val, Value::Null) {
if !def.nullable {
return false; }
} else if !validate_value_against_type(val, &def.field_type) {
return false; }
} else {
return false; }
}
for (key, def) in schema.iter() {
if !def.nullable && !map.contains_key(key) {
return false; }
}
true
}
(FieldType::Nested(_), _) => false,
(FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
(FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
_ => false,
}
}
fn validate_filter<S: SchemaProvider>(
filter: &Filter,
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
match filter {
Filter::Eq(field, value)
| Filter::Ne(field, value)
| Filter::Gt(field, value)
| Filter::Gte(field, value)
| Filter::Lt(field, value)
| Filter::Lte(field, value) => {
validate_filter_field(field, value, collection, ctx);
}
Filter::In(field, value) | Filter::NotIn(field, value) => {
if !matches!(value, Value::Array(_)) {
ctx.add_error(
ErrorCode::TypeMismatch,
format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
);
}
validate_filter_field_exists(field, collection, ctx);
}
Filter::ContainsAny(field, value) | Filter::ContainsAll(field, value) => {
if !matches!(value, Value::Array(_) | Value::Variable(_)) {
ctx.add_error(
ErrorCode::TypeMismatch,
format!("containsAny/containsAll on field '{}' expects an array", field),
);
}
validate_filter_field_exists(field, collection, ctx);
}
Filter::Contains(field, _)
| Filter::StartsWith(field, _)
| Filter::EndsWith(field, _)
| Filter::Matches(field, _) => {
if let Some(field_def) = collection.fields.get(field) {
if field_def.field_type != FieldType::SCALAR_STRING {
ctx.add_error(
ErrorCode::InvalidFilterOperator,
format!("String operator on non-string field '{}'", field),
);
}
} else {
validate_filter_field_exists(field, collection, ctx);
}
}
Filter::IsNull(field) | Filter::IsNotNull(field) => {
validate_filter_field_exists(field, collection, ctx);
}
Filter::And(filters) | Filter::Or(filters) => {
for f in filters {
validate_filter(f, collection, ctx);
}
}
Filter::Not(inner) => {
validate_filter(inner, collection, ctx);
}
}
}
fn validate_filter_field_exists<S: SchemaProvider>(
field: &str,
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
if field != "id" && !collection.fields.contains_key(field) {
ctx.add_error(
ErrorCode::UnknownField,
format!(
"Filter field '{}' does not exist in collection '{}'",
field, collection.name
),
);
}
}
fn validate_filter_field<S: SchemaProvider>(
field: &str,
value: &Value,
collection: &Collection,
ctx: &mut ValidationContext<'_, S>,
) {
if field == "id" {
return; }
if let Some(field_def) = collection.fields.get(field) {
if !is_type_compatible(&field_def.field_type, value) {
ctx.add_error(
ErrorCode::TypeMismatch,
format!(
"Type mismatch: field '{}' expects {:?}, got {:?}",
field,
field_def.field_type,
value_type_name(value)
),
);
}
} else {
ctx.add_error(
ErrorCode::UnknownField,
format!(
"Filter field '{}' does not exist in collection '{}'",
field, collection.name
),
);
}
}
fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
match (field_type, value) {
(_, Value::Null) => true, (_, Value::Variable(_)) => true, (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
(FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
(FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
(FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
(FieldType::Array(_), Value::Array(_)) => true,
(FieldType::Object, Value::Object(_)) => true,
(FieldType::Nested(_), Value::Object(_)) => true, (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true, (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true, (FieldType::Scalar(ScalarType::Any), _) => true, (FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(), _ => false,
}
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Boolean(_) => "boolean",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
Value::Variable(_) => "variable",
Value::Enum(_) => "enum",
}
}
fn report_unknown_filter_ops<S: SchemaProvider>(
value: &Value,
ctx: &mut ValidationContext<'_, S>,
) {
const KNOWN_OPS: &[&str] = &[
"eq", "ne", "gt", "gte", "lt", "lte",
"in", "nin", "contains", "startsWith", "endsWith",
"matches", "isNull", "isNotNull",
];
if let Value::Object(map) = value {
for (key, val) in map {
match key.as_str() {
"and" | "or" => {
if let Value::Array(arr) = val {
arr.iter().for_each(|v| report_unknown_filter_ops(v, ctx));
}
}
"not" => report_unknown_filter_ops(val, ctx),
field => {
if let Value::Object(ops) = val {
let has_known_op = ops.keys().any(|k| KNOWN_OPS.contains(&k.as_str()));
if has_known_op {
for op in ops.keys() {
if !KNOWN_OPS.contains(&op.as_str()) {
ctx.add_error(
ErrorCode::InvalidArgument,
format!(
"Unknown filter operator '{}' on field '{}'",
op, field
),
);
}
}
} else {
report_unknown_filter_ops(val, ctx);
}
}
}
}
}
}
}
fn extract_filter_from_value(value: &Value) -> Option<Filter> {
match value {
Value::Object(map) => {
let mut filters = Vec::new();
for (key, val) in map {
match key.as_str() {
"and" => {
if let Value::Array(arr) = val {
let sub_filters: Vec<Filter> =
arr.iter().filter_map(extract_filter_from_value).collect();
if !sub_filters.is_empty() {
filters.push(Filter::And(sub_filters));
}
}
}
"or" => {
if let Value::Array(arr) = val {
let sub_filters: Vec<Filter> =
arr.iter().filter_map(extract_filter_from_value).collect();
if !sub_filters.is_empty() {
filters.push(Filter::Or(sub_filters));
}
}
}
"not" => {
if let Some(inner) = extract_filter_from_value(val) {
filters.push(Filter::Not(Box::new(inner)));
}
}
field => {
if let Value::Object(ops) = val {
for (op, op_val) in ops {
let filter = match op.as_str() {
"eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
"ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
"gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
"gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
"lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
"lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
"in" => Some(Filter::In(field.to_string(), op_val.clone())),
"nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
"contains" => {
Some(Filter::Contains(field.to_string(), op_val.clone()))
}
"startsWith" => {
Some(Filter::StartsWith(field.to_string(), op_val.clone()))
}
"endsWith" => {
Some(Filter::EndsWith(field.to_string(), op_val.clone()))
}
"matches" => {
Some(Filter::Matches(field.to_string(), op_val.clone()))
}
"isNull" => Some(Filter::IsNull(field.to_string())),
"isNotNull" => Some(Filter::IsNotNull(field.to_string())),
_ => None,
};
if let Some(f) = filter {
filters.push(f);
}
}
}
}
}
}
match filters.len() {
0 => None,
1 => Some(filters.remove(0)),
_ => Some(Filter::And(filters)),
}
}
_ => None,
}
}
pub fn resolve_variables(
doc: &mut Document,
variables: &HashMap<String, ast::Value>,
) -> Result<(), ValidationError> {
for op in &mut doc.operations {
match op {
ast::Operation::Query(query) => {
resolve_in_fields(&mut query.selection_set, variables)?;
}
ast::Operation::Mutation(mutation) => {
for mut_op in &mut mutation.operations {
resolve_in_mutation_op(mut_op, variables)?;
}
}
ast::Operation::Subscription(sub) => {
resolve_in_fields(&mut sub.selection_set, variables)?;
}
ast::Operation::Schema(_) => {}
ast::Operation::Migration(_) => {}
ast::Operation::FragmentDefinition(fragment) => {
resolve_in_fields(&mut fragment.selection_set, variables)?;
}
ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
}
Ok(())
}
fn resolve_in_fields(
fields: &mut [Selection],
variables: &HashMap<String, ast::Value>,
) -> Result<(), ValidationError> {
for selection in fields {
match selection {
Selection::Field(field) => {
for arg in &mut field.arguments {
resolve_in_value(&mut arg.value, variables)?;
}
resolve_in_fields(&mut field.selection_set, variables)?;
}
Selection::InlineFragment(inline) => {
resolve_in_fields(&mut inline.selection_set, variables)?;
}
Selection::FragmentSpread(_) => {
}
}
}
Ok(())
}
fn resolve_in_mutation_op(
op: &mut ast::MutationOperation,
variables: &HashMap<String, ast::Value>,
) -> Result<(), ValidationError> {
match &mut op.operation {
ast::MutationOp::Insert { data, .. }
| ast::MutationOp::Update { data, .. }
| ast::MutationOp::Upsert { data, .. } => {
resolve_in_value(data, variables)?;
}
ast::MutationOp::InsertMany { data, .. } => {
for item in data {
resolve_in_value(item, variables)?;
}
}
ast::MutationOp::Delete { .. } => {}
ast::MutationOp::EnqueueJob { payload, .. } => {
resolve_in_value(payload, variables)?;
}
ast::MutationOp::EnqueueJobs { payloads, .. } => {
for p in payloads {
resolve_in_value(p, variables)?;
}
}
ast::MutationOp::Import { data, .. } => {
for item in data {
resolve_in_value(item, variables)?;
}
}
ast::MutationOp::Export { .. } => {}
ast::MutationOp::Transaction { operations } => {
for inner in operations {
resolve_in_mutation_op(inner, variables)?;
}
}
}
resolve_in_fields(&mut op.selection_set, variables)
}
fn resolve_in_value(
value: &mut Value,
variables: &HashMap<String, ast::Value>,
) -> Result<(), ValidationError> {
match value {
Value::Variable(name) => {
if let Some(resolved) = variables.get(name) {
*value = resolved.clone();
} else {
return Err(ValidationError::new(
ErrorCode::MissingRequiredVariable,
format!("Variable '{}' is not provided", name),
));
}
}
Value::Array(items) => {
for item in items {
resolve_in_value(item, variables)?;
}
}
Value::Object(map) => {
for v in map.values_mut() {
resolve_in_value(v, variables)?;
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::FieldDefinition;
fn create_test_schema() -> InMemorySchema {
let mut schema = InMemorySchema::new();
let mut users_fields = HashMap::new();
users_fields.insert(
"name".to_string(),
FieldDefinition {
field_type: FieldType::SCALAR_STRING,
unique: false,
indexed: false,
nullable: false,
..Default::default()
},
);
users_fields.insert(
"email".to_string(),
FieldDefinition {
field_type: FieldType::SCALAR_STRING,
unique: true,
indexed: true,
nullable: false,
..Default::default()
},
);
users_fields.insert(
"age".to_string(),
FieldDefinition {
field_type: FieldType::SCALAR_INT,
unique: false,
indexed: false,
nullable: false,
..Default::default()
},
);
users_fields.insert(
"active".to_string(),
FieldDefinition {
field_type: FieldType::SCALAR_BOOL,
unique: false,
indexed: false,
nullable: false,
..Default::default()
},
);
schema.add_collection(Collection {
name: "users".to_string(),
fields: users_fields,
});
schema
}
#[test]
fn test_validate_unknown_collection() {
let schema = create_test_schema();
let doc = Document {
operations: vec![ast::Operation::Query(Query {
name: None,
variable_definitions: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "nonexistent".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "id".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![],
})],
})],
variables_values: HashMap::new(),
})],
};
let result = validate_document(&doc, &schema, HashMap::new());
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| e.code == ErrorCode::UnknownCollection)
);
}
#[test]
fn test_validate_unknown_field() {
let schema = create_test_schema();
let doc = Document {
operations: vec![ast::Operation::Query(Query {
name: None,
variable_definitions: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "users".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "nonexistent_field".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![],
})],
})],
variables_values: HashMap::new(),
})],
};
let result = validate_document(&doc, &schema, HashMap::new());
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
}
#[test]
fn test_validate_missing_required_variable() {
let schema = create_test_schema();
let doc = Document {
operations: vec![ast::Operation::Query(Query {
name: Some("GetUsers".to_string()),
variable_definitions: vec![ast::VariableDefinition {
name: "minAge".to_string(),
var_type: ast::TypeAnnotation {
name: "Int".to_string(),
is_array: false,
is_required: true,
},
default_value: None,
}],
directives: vec![],
selection_set: vec![],
variables_values: HashMap::new(),
})],
};
let result = validate_document(&doc, &schema, HashMap::new());
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(
errors
.iter()
.any(|e| e.code == ErrorCode::MissingRequiredVariable)
);
}
#[test]
fn test_validate_valid_query() {
let schema = create_test_schema();
let doc = Document {
operations: vec![ast::Operation::Query(Query {
name: Some("GetUsers".to_string()),
variable_definitions: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "users".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![
Selection::Field(Field {
alias: None,
name: "id".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![],
}),
Selection::Field(Field {
alias: None,
name: "name".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![],
}),
Selection::Field(Field {
alias: None,
name: "email".to_string(),
arguments: vec![],
directives: vec![],
selection_set: vec![],
}),
],
})],
variables_values: HashMap::new(),
})],
};
let result = validate_document(&doc, &schema, HashMap::new());
assert!(result.is_ok());
}
#[test]
fn test_validate_filter_type_mismatch() {
let schema = create_test_schema();
let collection = schema.get_collection("users").unwrap();
let mut ctx = ValidationContext::new(&schema);
let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
validate_filter(&filter, collection, &mut ctx);
assert!(ctx.has_errors());
assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
}
#[test]
fn test_validate_filter_string_operator_on_int() {
let schema = create_test_schema();
let collection = schema.get_collection("users").unwrap();
let mut ctx = ValidationContext::new(&schema);
let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
validate_filter(&filter, collection, &mut ctx);
assert!(ctx.has_errors());
assert!(
ctx.errors
.iter()
.any(|e| e.code == ErrorCode::InvalidFilterOperator)
);
}
#[test]
fn test_resolve_variables() {
let mut doc = Document {
operations: vec![ast::Operation::Query(Query {
name: None,
variable_definitions: vec![],
directives: vec![],
selection_set: vec![Selection::Field(Field {
alias: None,
name: "users".to_string(),
arguments: vec![ast::Argument {
name: "limit".to_string(),
value: Value::Variable("pageSize".to_string()),
}],
directives: vec![],
selection_set: vec![],
})],
variables_values: HashMap::new(),
})],
};
let mut vars = HashMap::new();
vars.insert("pageSize".to_string(), Value::Int(10));
let result = resolve_variables(&mut doc, &vars);
assert!(result.is_ok());
if let ast::Operation::Query(query) = &doc.operations[0] {
if let Selection::Field(user_field) = &query.selection_set[0] {
let arg = &user_field.arguments[0];
assert!(matches!(arg.value, Value::Int(10)));
} else {
panic!("Expected Selection::Field");
}
} else {
panic!("Expected Query operation");
}
}
}