1use super::ast::{self, Document, Field, Filter, Mutation, Query, Selection, Subscription, Value};
10use crate::types::{Collection, FieldType, ScalarType};
11use std::collections::HashMap;
12use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ErrorCode {
17 UnknownCollection,
19 UnknownField,
21 InvalidInput,
23 TypeMismatch,
25 MissingRequiredVariable,
27 MissingOptionalVariable,
29 InvalidFilterOperator,
31 DuplicateAlias,
33 InvalidArgument,
35 UnknownDirective,
37}
38
39#[derive(Debug, Clone)]
41pub struct ValidationError {
42 pub code: ErrorCode,
43 pub message: String,
44 pub path: Option<String>,
45}
46
47impl fmt::Display for ValidationError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 match &self.path {
50 Some(path) => write!(f, "[{}] {}", path, self.message),
51 None => write!(f, "{}", self.message),
52 }
53 }
54}
55
56impl std::error::Error for ValidationError {}
57
58impl ValidationError {
59 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
60 Self {
61 code,
62 message: message.into(),
63 path: None,
64 }
65 }
66
67 pub fn with_path(mut self, path: impl Into<String>) -> Self {
68 self.path = Some(path.into());
69 self
70 }
71}
72
73pub type ValidationResult = Result<(), Vec<ValidationError>>;
75
76pub trait SchemaProvider {
79 fn get_collection(&self, name: &str) -> Option<&Collection>;
81
82 fn collection_exists(&self, name: &str) -> bool {
84 self.get_collection(name).is_some()
85 }
86}
87
88#[derive(Debug, Default)]
90pub struct InMemorySchema {
91 collections: HashMap<String, Collection>,
92}
93
94impl InMemorySchema {
95 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn add_collection(&mut self, collection: Collection) {
100 self.collections.insert(collection.name.clone(), collection);
101 }
102}
103
104impl SchemaProvider for InMemorySchema {
105 fn get_collection(&self, name: &str) -> Option<&Collection> {
106 self.collections.get(name)
107 }
108}
109
110pub struct ValidationContext<'a, S: SchemaProvider> {
112 pub schema: &'a S,
113 pub variables: HashMap<String, ast::Value>,
114 pub errors: Vec<ValidationError>,
115 current_path: Vec<String>,
116}
117
118impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
119 pub fn new(schema: &'a S) -> Self {
120 Self {
121 schema,
122 variables: HashMap::new(),
123 errors: Vec::new(),
124 current_path: Vec::new(),
125 }
126 }
127
128 pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
129 self.variables = variables;
130 self
131 }
132
133 fn push_path(&mut self, segment: &str) {
134 self.current_path.push(segment.to_string());
135 }
136
137 fn pop_path(&mut self) {
138 self.current_path.pop();
139 }
140
141 fn current_path_string(&self) -> Option<String> {
142 if self.current_path.is_empty() {
143 None
144 } else {
145 Some(self.current_path.join("."))
146 }
147 }
148
149 fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
150 let mut error = ValidationError::new(code, message);
151 error.path = self.current_path_string();
152 self.errors.push(error);
153 }
154
155 pub fn has_errors(&self) -> bool {
156 !self.errors.is_empty()
157 }
158
159 pub fn into_result(self) -> ValidationResult {
160 if self.errors.is_empty() {
161 Ok(())
162 } else {
163 Err(self.errors)
164 }
165 }
166}
167
168pub fn validate_document<S: SchemaProvider>(
170 doc: &Document,
171 schema: &S,
172 variables: HashMap<String, ast::Value>,
173) -> ValidationResult {
174 let mut ctx = ValidationContext::new(schema).with_variables(variables);
175
176 for (i, op) in doc.operations.iter().enumerate() {
177 ctx.push_path(&format!("operation[{}]", i));
178 match op {
179 ast::Operation::Query(query) => validate_query(query, &mut ctx),
180 ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
181 ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
182 ast::Operation::Schema(_) => {} ast::Operation::Migration(_) => {}
184 ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
188 ctx.pop_path();
189 }
190
191 ctx.into_result()
192}
193
194fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
196 if let Some(name) = &query.name {
197 ctx.push_path(name);
198 }
199
200 validate_variable_definitions(&query.variable_definitions, ctx);
202
203 for selection in &query.selection_set {
205 if let Selection::Field(field) = selection {
206 validate_field(field, ctx);
207 } else if let Selection::InlineFragment(inline) = selection {
208 validate_inline_fragment(inline, ctx);
209 }
210 }
211
212 if query.name.is_some() {
213 ctx.pop_path();
214 }
215}
216
217fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
219 if let Some(name) = &mutation.name {
220 ctx.push_path(name);
221 }
222
223 validate_variable_definitions(&mutation.variable_definitions, ctx);
225
226 for (i, op) in mutation.operations.iter().enumerate() {
228 ctx.push_path(&format!("mutation[{}]", i));
229 validate_mutation_operation(op, ctx);
230 ctx.pop_path();
231 }
232
233 if mutation.name.is_some() {
234 ctx.pop_path();
235 }
236}
237
238fn validate_inline_fragment<S: SchemaProvider>(
240 inline: &ast::InlineFragment,
241 ctx: &mut ValidationContext<'_, S>,
242) {
243 if !ctx.schema.collection_exists(&inline.type_condition) {
245 ctx.add_error(
246 ErrorCode::UnknownCollection,
247 format!(
248 "Unknown collection '{}' in inline fragment",
249 inline.type_condition
250 ),
251 );
252 return;
253 }
254
255 validate_selection_set(&inline.selection_set, &inline.type_condition, ctx);
257}
258
259fn validate_subscription<S: SchemaProvider>(
261 sub: &Subscription,
262 ctx: &mut ValidationContext<'_, S>,
263) {
264 if let Some(name) = &sub.name {
265 ctx.push_path(name);
266 }
267
268 validate_variable_definitions(&sub.variable_definitions, ctx);
270
271 for selection in &sub.selection_set {
273 if let Selection::Field(field) = selection {
274 validate_field(field, ctx);
275 }
276 }
277
278 if sub.name.is_some() {
279 ctx.pop_path();
280 }
281}
282
283fn validate_variable_definitions<S: SchemaProvider>(
285 definitions: &[ast::VariableDefinition],
286 ctx: &mut ValidationContext<'_, S>,
287) {
288 for def in definitions {
289 let var_name = &def.name;
290
291 if def.var_type.is_required && def.default_value.is_none() {
293 if !ctx.variables.contains_key(var_name) {
294 ctx.add_error(
295 ErrorCode::MissingRequiredVariable,
296 format!("Required variable '{}' is not provided", var_name),
297 );
298 }
299 }
300 }
301}
302
303fn validate_field<S: SchemaProvider>(field: &Field, ctx: &mut ValidationContext<'_, S>) {
305 let field_name = field.alias.as_ref().unwrap_or(&field.name);
306 ctx.push_path(field_name);
307
308 if field.selection_set.is_empty() {
310 } else {
312 let collection_name = &field.name;
314
315 if !ctx.schema.collection_exists(collection_name) {
316 ctx.add_error(
317 ErrorCode::UnknownCollection,
318 format!("Collection '{}' does not exist", collection_name),
319 );
320 } else {
321 if let Some(collection) = ctx.schema.get_collection(collection_name) {
323 validate_selection_set(&field.selection_set, &collection.name, ctx);
324 }
325 }
326
327 for arg in &field.arguments {
329 if arg.name == "where" || arg.name == "filter" {
330 if let Some(filter) = extract_filter_from_value(&arg.value) {
331 if let Some(collection) = ctx.schema.get_collection(collection_name) {
332 validate_filter(&filter, collection, ctx);
333 }
334 }
335 }
336 }
337 }
338
339 ctx.pop_path();
340}
341
342fn validate_selection_set<S: SchemaProvider>(
344 selections: &[Selection],
345 collection_name: &str,
346 ctx: &mut ValidationContext<'_, S>,
347) {
348 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
349
350 for selection in selections {
351 if let Selection::Field(field) = selection {
353 let display_name = field.alias.as_ref().unwrap_or(&field.name);
354
355 if aliases_seen.contains_key(display_name) {
357 ctx.add_error(
358 ErrorCode::DuplicateAlias,
359 format!("Duplicate field/alias '{}' in selection", display_name),
360 );
361 }
362 aliases_seen.insert(display_name.clone(), true);
363
364 if let Some(collection) = ctx.schema.get_collection(collection_name) {
367 let field_name = &field.name;
368 if !field_name.starts_with("__") && field_name != "id" {
369 if !collection.fields.contains_key(field_name) {
370 ctx.add_error(
371 ErrorCode::UnknownField,
372 format!(
373 "Field '{}' does not exist in collection '{}'",
374 field_name, collection.name
375 ),
376 );
377 }
378 }
379
380 validate_field(field, ctx);
382 }
383 } else if let Selection::InlineFragment(inline) = selection {
384 validate_inline_fragment(inline, ctx);
385 }
386 }
387}
388
389fn validate_mutation_operation<S: SchemaProvider>(
391 op: &ast::MutationOperation,
392 ctx: &mut ValidationContext<'_, S>,
393) {
394 match &op.operation {
395 ast::MutationOp::Insert { collection, data } => {
396 if let Some(col_def) = ctx.schema.get_collection(collection) {
397 validate_object_against_schema(data, col_def, ctx);
398 } else {
399 ctx.add_error(
400 ErrorCode::UnknownCollection,
401 format!("Collection '{}' does not exist", collection),
402 );
403 }
404 }
405 ast::MutationOp::InsertMany { collection, data } => {
406 if let Some(col_def) = ctx.schema.get_collection(collection) {
407 for item in data {
408 validate_object_against_schema(item, col_def, ctx);
409 }
410 } else {
411 ctx.add_error(
412 ErrorCode::UnknownCollection,
413 format!("Collection '{}' does not exist", collection),
414 );
415 }
416 }
417 ast::MutationOp::Update {
418 collection, data, ..
419 }
420 | ast::MutationOp::Upsert {
421 collection, data, ..
422 } => {
423 if let Some(col_def) = ctx.schema.get_collection(collection) {
424 validate_partial_object(data, col_def, ctx);
426 } else {
427 ctx.add_error(
428 ErrorCode::UnknownCollection,
429 format!("Collection '{}' does not exist", collection),
430 );
431 }
432 }
433 ast::MutationOp::Delete { collection, .. } => {
434 if !ctx.schema.collection_exists(collection) {
435 ctx.add_error(
436 ErrorCode::UnknownCollection,
437 format!("Collection '{}' does not exist", collection),
438 );
439 }
440 }
441 ast::MutationOp::EnqueueJob { .. } => {
442 }
444 ast::MutationOp::Transaction { operations } => {
445 for (i, inner_op) in operations.iter().enumerate() {
446 ctx.push_path(&format!("tx[{}]", i));
447 validate_mutation_operation(inner_op, ctx);
448 ctx.pop_path();
449 }
450 }
451 }
452}
453
454fn validate_object_against_schema<S: SchemaProvider>(
456 value: &Value,
457 collection: &Collection,
458 ctx: &mut ValidationContext<'_, S>,
459) {
460 match value {
461 Value::Object(map) => {
462 for (key, val) in map {
464 if let Some(field_def) = collection.fields.get(key) {
465 if matches!(val, Value::Null) && !field_def.nullable {
467 ctx.add_error(
468 ErrorCode::InvalidInput,
469 format!("Field '{}' cannot be null", key),
470 );
471 } else if !matches!(val, Value::Null)
472 && !validate_value_against_type(val, &field_def.field_type)
473 {
474 ctx.add_error(
475 ErrorCode::TypeMismatch,
476 format!(
477 "Type mismatch for field '{}': expected {:?}, got {:?}",
478 key,
479 field_def.field_type,
480 value_type_name(val)
481 ),
482 );
483 }
484 } else if key != "id" {
485 ctx.add_error(
486 ErrorCode::UnknownField,
487 format!(
488 "Field '{}' not defined in collection '{}'",
489 key, collection.name
490 ),
491 );
492 }
493 }
494
495 for (name, def) in &collection.fields {
497 if !def.nullable && !map.contains_key(name) {
498 ctx.add_error(
499 ErrorCode::InvalidInput,
500 format!("Missing required field '{}'", name),
501 );
502 }
503 }
504 }
505 _ => {
506 ctx.add_error(
507 ErrorCode::TypeMismatch,
508 format!(
509 "Expected object for collection '{}', got {:?}",
510 collection.name,
511 value_type_name(value)
512 ),
513 );
514 }
515 }
516}
517
518fn validate_partial_object<S: SchemaProvider>(
521 value: &Value,
522 collection: &Collection,
523 ctx: &mut ValidationContext<'_, S>,
524) {
525 match value {
526 Value::Object(map) => {
527 for (key, val) in map {
529 if let Some(field_def) = collection.fields.get(key) {
530 if matches!(val, Value::Null) && !field_def.nullable {
532 ctx.add_error(
533 ErrorCode::InvalidInput,
534 format!("Field '{}' cannot be null", key),
535 );
536 } else if !matches!(val, Value::Null)
537 && !validate_value_against_type(val, &field_def.field_type)
538 {
539 ctx.add_error(
540 ErrorCode::TypeMismatch,
541 format!(
542 "Type mismatch for field '{}': expected {:?}, got {:?}",
543 key,
544 field_def.field_type,
545 value_type_name(val)
546 ),
547 );
548 }
549 } else if key != "id" {
550 ctx.add_error(
551 ErrorCode::UnknownField,
552 format!(
553 "Field '{}' not defined in collection '{}'",
554 key, collection.name
555 ),
556 );
557 }
558 }
559 }
560 _ => {
561 ctx.add_error(
562 ErrorCode::TypeMismatch,
563 format!(
564 "Expected object for collection '{}', got {:?}",
565 collection.name,
566 value_type_name(value)
567 ),
568 );
569 }
570 }
571}
572
573fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
575 use crate::types::ScalarType;
576 match (expected, value) {
577 (FieldType::Scalar(ScalarType::Any), _) => true,
579 (_, Value::Null) => true,
580 (_, Value::Variable(_)) => true,
581
582 (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
583 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
584 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
585 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
586 (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
587 (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true,
588
589 (FieldType::Array(scalar_inner), Value::Array(items)) => {
591 let inner_field_type = FieldType::Scalar(scalar_inner.clone());
593 items
594 .iter()
595 .all(|item| validate_value_against_type(item, &inner_field_type))
596 }
597
598 (FieldType::Object, Value::Object(_)) => true,
600 (FieldType::Nested(schema), Value::Object(map)) => {
602 for (key, val) in map {
604 if let Some(def) = schema.get(key) {
605 if matches!(val, Value::Null) {
606 if !def.nullable {
607 return false; }
609 } else if !validate_value_against_type(val, &def.field_type) {
610 return false; }
612 } else {
613 return false; }
615 }
616 for (key, def) in schema.iter() {
618 if !def.nullable && !map.contains_key(key) {
619 return false; }
621 }
622 true
623 }
624 (FieldType::Nested(_), _) => false, (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
628 (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
630
631 _ => false,
632 }
633}
634
635fn validate_filter<S: SchemaProvider>(
637 filter: &Filter,
638 collection: &Collection,
639 ctx: &mut ValidationContext<'_, S>,
640) {
641 match filter {
642 Filter::Eq(field, value)
643 | Filter::Ne(field, value)
644 | Filter::Gt(field, value)
645 | Filter::Gte(field, value)
646 | Filter::Lt(field, value)
647 | Filter::Lte(field, value) => {
648 validate_filter_field(field, value, collection, ctx);
649 }
650 Filter::In(field, value) | Filter::NotIn(field, value) => {
651 if !matches!(value, Value::Array(_)) {
653 ctx.add_error(
654 ErrorCode::TypeMismatch,
655 format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
656 );
657 }
658 validate_filter_field_exists(field, collection, ctx);
659 }
660 Filter::Contains(field, _)
661 | Filter::StartsWith(field, _)
662 | Filter::EndsWith(field, _)
663 | Filter::Matches(field, _) => {
664 if let Some(field_def) = collection.fields.get(field) {
666 if field_def.field_type != FieldType::String {
667 ctx.add_error(
668 ErrorCode::InvalidFilterOperator,
669 format!("String operator on non-string field '{}'", field),
670 );
671 }
672 } else {
673 validate_filter_field_exists(field, collection, ctx);
674 }
675 }
676 Filter::IsNull(field) | Filter::IsNotNull(field) => {
677 validate_filter_field_exists(field, collection, ctx);
678 }
679 Filter::And(filters) | Filter::Or(filters) => {
680 for f in filters {
681 validate_filter(f, collection, ctx);
682 }
683 }
684 Filter::Not(inner) => {
685 validate_filter(inner, collection, ctx);
686 }
687 }
688}
689
690fn validate_filter_field_exists<S: SchemaProvider>(
692 field: &str,
693 collection: &Collection,
694 ctx: &mut ValidationContext<'_, S>,
695) {
696 if field != "id" && !collection.fields.contains_key(field) {
697 ctx.add_error(
698 ErrorCode::UnknownField,
699 format!(
700 "Filter field '{}' does not exist in collection '{}'",
701 field, collection.name
702 ),
703 );
704 }
705}
706
707fn validate_filter_field<S: SchemaProvider>(
709 field: &str,
710 value: &Value,
711 collection: &Collection,
712 ctx: &mut ValidationContext<'_, S>,
713) {
714 if field == "id" {
715 return; }
717
718 if let Some(field_def) = collection.fields.get(field) {
719 if !is_type_compatible(&field_def.field_type, value) {
721 ctx.add_error(
722 ErrorCode::TypeMismatch,
723 format!(
724 "Type mismatch: field '{}' expects {:?}, got {:?}",
725 field,
726 field_def.field_type,
727 value_type_name(value)
728 ),
729 );
730 }
731 } else {
732 ctx.add_error(
733 ErrorCode::UnknownField,
734 format!(
735 "Filter field '{}' does not exist in collection '{}'",
736 field, collection.name
737 ),
738 );
739 }
740}
741
742fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
744 match (field_type, value) {
745 (_, Value::Null) => true, (_, Value::Variable(_)) => true, (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
748 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
749 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
750 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
752 (FieldType::Array(_), Value::Array(_)) => true,
753 (FieldType::Object, Value::Object(_)) => true,
754 (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(_)) => true, _ => false,
760 }
761}
762
763fn value_type_name(value: &Value) -> &'static str {
765 match value {
766 Value::Null => "null",
767 Value::Boolean(_) => "boolean",
768 Value::Int(_) => "int",
769 Value::Float(_) => "float",
770 Value::String(_) => "string",
771 Value::Array(_) => "array",
772 Value::Object(_) => "object",
773 Value::Variable(_) => "variable",
774 Value::Enum(_) => "enum",
775 }
776}
777
778fn extract_filter_from_value(value: &Value) -> Option<Filter> {
780 match value {
781 Value::Object(map) => {
782 let mut filters = Vec::new();
783
784 for (key, val) in map {
785 match key.as_str() {
786 "and" => {
787 if let Value::Array(arr) = val {
788 let sub_filters: Vec<Filter> =
789 arr.iter().filter_map(extract_filter_from_value).collect();
790 if !sub_filters.is_empty() {
791 filters.push(Filter::And(sub_filters));
792 }
793 }
794 }
795 "or" => {
796 if let Value::Array(arr) = val {
797 let sub_filters: Vec<Filter> =
798 arr.iter().filter_map(extract_filter_from_value).collect();
799 if !sub_filters.is_empty() {
800 filters.push(Filter::Or(sub_filters));
801 }
802 }
803 }
804 "not" => {
805 if let Some(inner) = extract_filter_from_value(val) {
806 filters.push(Filter::Not(Box::new(inner)));
807 }
808 }
809 field => {
810 if let Value::Object(ops) = val {
812 for (op, op_val) in ops {
813 let filter = match op.as_str() {
814 "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
815 "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
816 "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
817 "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
818 "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
819 "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
820 "in" => Some(Filter::In(field.to_string(), op_val.clone())),
821 "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
822 "contains" => {
823 Some(Filter::Contains(field.to_string(), op_val.clone()))
824 }
825 "startsWith" => {
826 Some(Filter::StartsWith(field.to_string(), op_val.clone()))
827 }
828 "endsWith" => {
829 Some(Filter::EndsWith(field.to_string(), op_val.clone()))
830 }
831 "isNull" => Some(Filter::IsNull(field.to_string())),
832 "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
833 _ => None,
834 };
835 if let Some(f) = filter {
836 filters.push(f);
837 }
838 }
839 }
840 }
841 }
842 }
843
844 match filters.len() {
845 0 => None,
846 1 => Some(filters.remove(0)),
847 _ => Some(Filter::And(filters)),
848 }
849 }
850 _ => None,
851 }
852}
853
854pub fn resolve_variables(
856 doc: &mut Document,
857 variables: &HashMap<String, ast::Value>,
858) -> Result<(), ValidationError> {
859 for op in &mut doc.operations {
860 match op {
861 ast::Operation::Query(query) => {
862 resolve_in_fields(&mut query.selection_set, variables)?;
863 }
864 ast::Operation::Mutation(mutation) => {
865 for mut_op in &mut mutation.operations {
866 resolve_in_mutation_op(mut_op, variables)?;
867 }
868 }
869 ast::Operation::Subscription(sub) => {
870 resolve_in_fields(&mut sub.selection_set, variables)?;
871 }
872 ast::Operation::Schema(_) => {}
873 ast::Operation::Migration(_) => {}
874 ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
878 }
879 Ok(())
880}
881
882fn resolve_in_fields(
883 fields: &mut [Selection],
884 variables: &HashMap<String, ast::Value>,
885) -> Result<(), ValidationError> {
886 for selection in fields {
887 match selection {
888 Selection::Field(field) => {
889 for arg in &mut field.arguments {
891 resolve_in_value(&mut arg.value, variables)?;
892 }
893 resolve_in_fields(&mut field.selection_set, variables)?;
895 }
896 Selection::InlineFragment(inline) => {
897 resolve_in_fields(&mut inline.selection_set, variables)?;
898 }
899 Selection::FragmentSpread(_) => {
900 }
902 }
903 }
904 Ok(())
905}
906
907fn resolve_in_mutation_op(
908 op: &mut ast::MutationOperation,
909 variables: &HashMap<String, ast::Value>,
910) -> Result<(), ValidationError> {
911 match &mut op.operation {
912 ast::MutationOp::Insert { data, .. }
913 | ast::MutationOp::Update { data, .. }
914 | ast::MutationOp::Upsert { data, .. } => {
915 resolve_in_value(data, variables)?;
916 }
917 ast::MutationOp::InsertMany { data, .. } => {
918 for item in data {
919 resolve_in_value(item, variables)?;
920 }
921 }
922 ast::MutationOp::Delete { .. } => {}
923 ast::MutationOp::EnqueueJob { payload, .. } => {
924 resolve_in_value(payload, variables)?;
925 }
926 ast::MutationOp::Transaction { operations } => {
927 for inner in operations {
928 resolve_in_mutation_op(inner, variables)?;
929 }
930 }
931 }
932 resolve_in_fields(&mut op.selection_set, variables)
933}
934
935fn resolve_in_value(
936 value: &mut Value,
937 variables: &HashMap<String, ast::Value>,
938) -> Result<(), ValidationError> {
939 match value {
940 Value::Variable(name) => {
941 if let Some(resolved) = variables.get(name) {
942 *value = resolved.clone();
943 } else {
944 return Err(ValidationError::new(
945 ErrorCode::MissingRequiredVariable,
946 format!("Variable '{}' is not provided", name),
947 ));
948 }
949 }
950 Value::Array(items) => {
951 for item in items {
952 resolve_in_value(item, variables)?;
953 }
954 }
955 Value::Object(map) => {
956 for v in map.values_mut() {
957 resolve_in_value(v, variables)?;
958 }
959 }
960 _ => {}
961 }
962 Ok(())
963}
964
965#[cfg(test)]
966mod tests {
967 use super::*;
968 use crate::types::FieldDefinition;
969
970 fn create_test_schema() -> InMemorySchema {
971 let mut schema = InMemorySchema::new();
972
973 let mut users_fields = HashMap::new();
974 users_fields.insert(
975 "name".to_string(),
976 FieldDefinition {
977 field_type: FieldType::String,
978 unique: false,
979 indexed: false,
980 nullable: false,
981 },
982 );
983 users_fields.insert(
984 "email".to_string(),
985 FieldDefinition {
986 field_type: FieldType::String,
987 unique: true,
988 indexed: true,
989 nullable: false,
990 },
991 );
992 users_fields.insert(
993 "age".to_string(),
994 FieldDefinition {
995 field_type: FieldType::Int,
996 unique: false,
997 indexed: false,
998 nullable: false,
999 },
1000 );
1001 users_fields.insert(
1002 "active".to_string(),
1003 FieldDefinition {
1004 field_type: FieldType::Bool,
1005 unique: false,
1006 indexed: false,
1007 nullable: false,
1008 },
1009 );
1010
1011 schema.add_collection(Collection {
1012 name: "users".to_string(),
1013 fields: users_fields,
1014 });
1015
1016 schema
1017 }
1018
1019 #[test]
1020 fn test_validate_unknown_collection() {
1021 let schema = create_test_schema();
1022 let doc = Document {
1023 operations: vec![ast::Operation::Query(Query {
1024 name: None,
1025 variable_definitions: vec![],
1026 directives: vec![],
1027 selection_set: vec![Selection::Field(Field {
1028 alias: None,
1029 name: "nonexistent".to_string(),
1030 arguments: vec![],
1031 directives: vec![],
1032 selection_set: vec![Selection::Field(Field {
1033 alias: None,
1034 name: "id".to_string(),
1035 arguments: vec![],
1036 directives: vec![],
1037 selection_set: vec![],
1038 })],
1039 })],
1040 variables_values: HashMap::new(),
1041 })],
1042 };
1043
1044 let result = validate_document(&doc, &schema, HashMap::new());
1045 assert!(result.is_err());
1046 let errors = result.unwrap_err();
1047 assert!(
1048 errors
1049 .iter()
1050 .any(|e| e.code == ErrorCode::UnknownCollection)
1051 );
1052 }
1053
1054 #[test]
1055 fn test_validate_unknown_field() {
1056 let schema = create_test_schema();
1057 let doc = Document {
1058 operations: vec![ast::Operation::Query(Query {
1059 name: None,
1060 variable_definitions: vec![],
1061 directives: vec![],
1062 selection_set: vec![Selection::Field(Field {
1063 alias: None,
1064 name: "users".to_string(),
1065 arguments: vec![],
1066 directives: vec![],
1067 selection_set: vec![Selection::Field(Field {
1068 alias: None,
1069 name: "nonexistent_field".to_string(),
1070 arguments: vec![],
1071 directives: vec![],
1072 selection_set: vec![],
1073 })],
1074 })],
1075 variables_values: HashMap::new(),
1076 })],
1077 };
1078
1079 let result = validate_document(&doc, &schema, HashMap::new());
1080 assert!(result.is_err());
1081 let errors = result.unwrap_err();
1082 assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1083 }
1084
1085 #[test]
1086 fn test_validate_missing_required_variable() {
1087 let schema = create_test_schema();
1088 let doc = Document {
1089 operations: vec![ast::Operation::Query(Query {
1090 name: Some("GetUsers".to_string()),
1091 variable_definitions: vec![ast::VariableDefinition {
1092 name: "minAge".to_string(),
1093 var_type: ast::TypeAnnotation {
1094 name: "Int".to_string(),
1095 is_array: false,
1096 is_required: true,
1097 },
1098 default_value: None,
1099 }],
1100 directives: vec![],
1101 selection_set: vec![],
1102 variables_values: HashMap::new(),
1103 })],
1104 };
1105
1106 let result = validate_document(&doc, &schema, HashMap::new());
1108 assert!(result.is_err());
1109 let errors = result.unwrap_err();
1110 assert!(
1111 errors
1112 .iter()
1113 .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1114 );
1115 }
1116
1117 #[test]
1118 fn test_validate_valid_query() {
1119 let schema = create_test_schema();
1120 let doc = Document {
1121 operations: vec![ast::Operation::Query(Query {
1122 name: Some("GetUsers".to_string()),
1123 variable_definitions: vec![],
1124 directives: vec![],
1125 selection_set: vec![Selection::Field(Field {
1126 alias: None,
1127 name: "users".to_string(),
1128 arguments: vec![],
1129 directives: vec![],
1130 selection_set: vec![
1131 Selection::Field(Field {
1132 alias: None,
1133 name: "id".to_string(),
1134 arguments: vec![],
1135 directives: vec![],
1136 selection_set: vec![],
1137 }),
1138 Selection::Field(Field {
1139 alias: None,
1140 name: "name".to_string(),
1141 arguments: vec![],
1142 directives: vec![],
1143 selection_set: vec![],
1144 }),
1145 Selection::Field(Field {
1146 alias: None,
1147 name: "email".to_string(),
1148 arguments: vec![],
1149 directives: vec![],
1150 selection_set: vec![],
1151 }),
1152 ],
1153 })],
1154 variables_values: HashMap::new(),
1155 })],
1156 };
1157
1158 let result = validate_document(&doc, &schema, HashMap::new());
1159 assert!(result.is_ok());
1160 }
1161
1162 #[test]
1163 fn test_validate_filter_type_mismatch() {
1164 let schema = create_test_schema();
1165 let collection = schema.get_collection("users").unwrap();
1166 let mut ctx = ValidationContext::new(&schema);
1167
1168 let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1170 validate_filter(&filter, collection, &mut ctx);
1171
1172 assert!(ctx.has_errors());
1173 assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1174 }
1175
1176 #[test]
1177 fn test_validate_filter_string_operator_on_int() {
1178 let schema = create_test_schema();
1179 let collection = schema.get_collection("users").unwrap();
1180 let mut ctx = ValidationContext::new(&schema);
1181
1182 let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1184 validate_filter(&filter, collection, &mut ctx);
1185
1186 assert!(ctx.has_errors());
1187 assert!(
1188 ctx.errors
1189 .iter()
1190 .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1191 );
1192 }
1193
1194 #[test]
1195 fn test_resolve_variables() {
1196 let mut doc = Document {
1197 operations: vec![ast::Operation::Query(Query {
1198 name: None,
1199 variable_definitions: vec![],
1200 directives: vec![],
1201 selection_set: vec![Selection::Field(Field {
1202 alias: None,
1203 name: "users".to_string(),
1204 arguments: vec![ast::Argument {
1205 name: "limit".to_string(),
1206 value: Value::Variable("pageSize".to_string()),
1207 }],
1208 directives: vec![],
1209 selection_set: vec![],
1210 })],
1211 variables_values: HashMap::new(),
1212 })],
1213 };
1214
1215 let mut vars = HashMap::new();
1216 vars.insert("pageSize".to_string(), Value::Int(10));
1217
1218 let result = resolve_variables(&mut doc, &vars);
1219 assert!(result.is_ok());
1220
1221 if let ast::Operation::Query(query) = &doc.operations[0] {
1223 if let Selection::Field(user_field) = &query.selection_set[0] {
1224 let arg = &user_field.arguments[0];
1225 assert!(matches!(arg.value, Value::Int(10)));
1226 } else {
1227 panic!("Expected Selection::Field");
1228 }
1229 } else {
1230 panic!("Expected Query operation");
1231 }
1232 }
1233}