1use super::ast::{self, Document, Field, Filter, Mutation, Query, Subscription, Value};
10use crate::types::{Collection, FieldType};
11use std::collections::HashMap;
12use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ErrorCode {
17 UnknownCollection,
19 UnknownField,
21 TypeMismatch,
23 MissingRequiredVariable,
25 MissingOptionalVariable,
27 InvalidFilterOperator,
29 DuplicateAlias,
31 InvalidArgument,
33 UnknownDirective,
35}
36
37#[derive(Debug, Clone)]
39pub struct ValidationError {
40 pub code: ErrorCode,
41 pub message: String,
42 pub path: Option<String>,
43}
44
45impl fmt::Display for ValidationError {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match &self.path {
48 Some(path) => write!(f, "[{}] {}", path, self.message),
49 None => write!(f, "{}", self.message),
50 }
51 }
52}
53
54impl std::error::Error for ValidationError {}
55
56impl ValidationError {
57 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
58 Self {
59 code,
60 message: message.into(),
61 path: None,
62 }
63 }
64
65 pub fn with_path(mut self, path: impl Into<String>) -> Self {
66 self.path = Some(path.into());
67 self
68 }
69}
70
71pub type ValidationResult = Result<(), Vec<ValidationError>>;
73
74pub trait SchemaProvider {
77 fn get_collection(&self, name: &str) -> Option<&Collection>;
79
80 fn collection_exists(&self, name: &str) -> bool {
82 self.get_collection(name).is_some()
83 }
84}
85
86#[derive(Debug, Default)]
88pub struct InMemorySchema {
89 collections: HashMap<String, Collection>,
90}
91
92impl InMemorySchema {
93 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn add_collection(&mut self, collection: Collection) {
98 self.collections.insert(collection.name.clone(), collection);
99 }
100}
101
102impl SchemaProvider for InMemorySchema {
103 fn get_collection(&self, name: &str) -> Option<&Collection> {
104 self.collections.get(name)
105 }
106}
107
108pub struct ValidationContext<'a, S: SchemaProvider> {
110 pub schema: &'a S,
111 pub variables: HashMap<String, ast::Value>,
112 pub errors: Vec<ValidationError>,
113 current_path: Vec<String>,
114}
115
116impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
117 pub fn new(schema: &'a S) -> Self {
118 Self {
119 schema,
120 variables: HashMap::new(),
121 errors: Vec::new(),
122 current_path: Vec::new(),
123 }
124 }
125
126 pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
127 self.variables = variables;
128 self
129 }
130
131 fn push_path(&mut self, segment: &str) {
132 self.current_path.push(segment.to_string());
133 }
134
135 fn pop_path(&mut self) {
136 self.current_path.pop();
137 }
138
139 fn current_path_string(&self) -> Option<String> {
140 if self.current_path.is_empty() {
141 None
142 } else {
143 Some(self.current_path.join("."))
144 }
145 }
146
147 fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
148 let mut error = ValidationError::new(code, message);
149 error.path = self.current_path_string();
150 self.errors.push(error);
151 }
152
153 pub fn has_errors(&self) -> bool {
154 !self.errors.is_empty()
155 }
156
157 pub fn into_result(self) -> ValidationResult {
158 if self.errors.is_empty() {
159 Ok(())
160 } else {
161 Err(self.errors)
162 }
163 }
164}
165
166pub fn validate_document<S: SchemaProvider>(
168 doc: &Document,
169 schema: &S,
170 variables: HashMap<String, ast::Value>,
171) -> ValidationResult {
172 let mut ctx = ValidationContext::new(schema).with_variables(variables);
173
174 for (i, op) in doc.operations.iter().enumerate() {
175 ctx.push_path(&format!("operation[{}]", i));
176 match op {
177 ast::Operation::Query(query) => validate_query(query, &mut ctx),
178 ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
179 ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
180 ast::Operation::Schema(_) => {} ast::Operation::Migration(_) => {}
182 ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
186 ctx.pop_path();
187 }
188
189 ctx.into_result()
190}
191
192fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
194 if let Some(name) = &query.name {
195 ctx.push_path(name);
196 }
197
198 validate_variable_definitions(&query.variable_definitions, ctx);
200
201 for field in &query.selection_set {
203 validate_field(field, ctx);
204 }
205
206 if query.name.is_some() {
207 ctx.pop_path();
208 }
209}
210
211fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
213 if let Some(name) = &mutation.name {
214 ctx.push_path(name);
215 }
216
217 validate_variable_definitions(&mutation.variable_definitions, ctx);
219
220 for (i, op) in mutation.operations.iter().enumerate() {
222 ctx.push_path(&format!("mutation[{}]", i));
223 validate_mutation_operation(op, ctx);
224 ctx.pop_path();
225 }
226
227 if mutation.name.is_some() {
228 ctx.pop_path();
229 }
230}
231
232fn validate_subscription<S: SchemaProvider>(
234 sub: &Subscription,
235 ctx: &mut ValidationContext<'_, S>,
236) {
237 if let Some(name) = &sub.name {
238 ctx.push_path(name);
239 }
240
241 validate_variable_definitions(&sub.variable_definitions, ctx);
243
244 for field in &sub.selection_set {
246 validate_field(field, ctx);
247 }
248
249 if sub.name.is_some() {
250 ctx.pop_path();
251 }
252}
253
254fn validate_variable_definitions<S: SchemaProvider>(
256 definitions: &[ast::VariableDefinition],
257 ctx: &mut ValidationContext<'_, S>,
258) {
259 for def in definitions {
260 let var_name = &def.name;
261
262 if def.var_type.is_required && def.default_value.is_none() {
264 if !ctx.variables.contains_key(var_name) {
265 ctx.add_error(
266 ErrorCode::MissingRequiredVariable,
267 format!("Required variable '{}' is not provided", var_name),
268 );
269 }
270 }
271 }
272}
273
274fn validate_field<S: SchemaProvider>(field: &Field, ctx: &mut ValidationContext<'_, S>) {
276 let field_name = field.alias.as_ref().unwrap_or(&field.name);
277 ctx.push_path(field_name);
278
279 if field.selection_set.is_empty() {
281 } else {
283 let collection_name = &field.name;
285
286 if !ctx.schema.collection_exists(collection_name) {
287 ctx.add_error(
288 ErrorCode::UnknownCollection,
289 format!("Collection '{}' does not exist", collection_name),
290 );
291 } else {
292 if let Some(collection) = ctx.schema.get_collection(collection_name) {
294 validate_selection_set(&field.selection_set, collection, ctx);
295 }
296 }
297
298 for arg in &field.arguments {
300 if arg.name == "where" || arg.name == "filter" {
301 if let Some(filter) = extract_filter_from_value(&arg.value) {
302 if let Some(collection) = ctx.schema.get_collection(collection_name) {
303 validate_filter(&filter, collection, ctx);
304 }
305 }
306 }
307 }
308 }
309
310 ctx.pop_path();
311}
312
313fn validate_selection_set<S: SchemaProvider>(
315 fields: &[Field],
316 collection: &Collection,
317 ctx: &mut ValidationContext<'_, S>,
318) {
319 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
320
321 for field in fields {
322 let display_name = field.alias.as_ref().unwrap_or(&field.name);
323
324 if aliases_seen.contains_key(display_name) {
326 ctx.add_error(
327 ErrorCode::DuplicateAlias,
328 format!("Duplicate field/alias '{}' in selection", display_name),
329 );
330 }
331 aliases_seen.insert(display_name.clone(), true);
332
333 let field_name = &field.name;
335 if !field_name.starts_with("__") && field_name != "id" {
336 if !collection.fields.contains_key(field_name) {
337 ctx.add_error(
338 ErrorCode::UnknownField,
339 format!(
340 "Field '{}' does not exist in collection '{}'",
341 field_name, collection.name
342 ),
343 );
344 }
345 }
346 }
347}
348
349fn validate_mutation_operation<S: SchemaProvider>(
351 op: &ast::MutationOperation,
352 ctx: &mut ValidationContext<'_, S>,
353) {
354 match &op.operation {
355 ast::MutationOp::Insert { collection, .. }
356 | ast::MutationOp::Update { collection, .. }
357 | ast::MutationOp::Upsert { collection, .. }
358 | ast::MutationOp::Delete { collection, .. } => {
359 if !ctx.schema.collection_exists(collection) {
360 ctx.add_error(
361 ErrorCode::UnknownCollection,
362 format!("Collection '{}' does not exist", collection),
363 );
364 }
365 }
366 ast::MutationOp::InsertMany { collection, .. } => {
367 if !ctx.schema.collection_exists(collection) {
368 ctx.add_error(
369 ErrorCode::UnknownCollection,
370 format!("Collection '{}' does not exist", collection),
371 );
372 }
373 }
374 ast::MutationOp::EnqueueJob { .. } => {
375 }
377 ast::MutationOp::Transaction { operations } => {
378 for (i, inner_op) in operations.iter().enumerate() {
379 ctx.push_path(&format!("tx[{}]", i));
380 validate_mutation_operation(inner_op, ctx);
381 ctx.pop_path();
382 }
383 }
384 }
385}
386
387fn validate_filter<S: SchemaProvider>(
389 filter: &Filter,
390 collection: &Collection,
391 ctx: &mut ValidationContext<'_, S>,
392) {
393 match filter {
394 Filter::Eq(field, value)
395 | Filter::Ne(field, value)
396 | Filter::Gt(field, value)
397 | Filter::Gte(field, value)
398 | Filter::Lt(field, value)
399 | Filter::Lte(field, value) => {
400 validate_filter_field(field, value, collection, ctx);
401 }
402 Filter::In(field, value) | Filter::NotIn(field, value) => {
403 if !matches!(value, Value::Array(_)) {
405 ctx.add_error(
406 ErrorCode::TypeMismatch,
407 format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
408 );
409 }
410 validate_filter_field_exists(field, collection, ctx);
411 }
412 Filter::Contains(field, _)
413 | Filter::StartsWith(field, _)
414 | Filter::EndsWith(field, _)
415 | Filter::Matches(field, _) => {
416 if let Some(field_def) = collection.fields.get(field) {
418 if field_def.field_type != FieldType::String {
419 ctx.add_error(
420 ErrorCode::InvalidFilterOperator,
421 format!("String operator on non-string field '{}'", field),
422 );
423 }
424 } else {
425 validate_filter_field_exists(field, collection, ctx);
426 }
427 }
428 Filter::IsNull(field) | Filter::IsNotNull(field) => {
429 validate_filter_field_exists(field, collection, ctx);
430 }
431 Filter::And(filters) | Filter::Or(filters) => {
432 for f in filters {
433 validate_filter(f, collection, ctx);
434 }
435 }
436 Filter::Not(inner) => {
437 validate_filter(inner, collection, ctx);
438 }
439 }
440}
441
442fn validate_filter_field_exists<S: SchemaProvider>(
444 field: &str,
445 collection: &Collection,
446 ctx: &mut ValidationContext<'_, S>,
447) {
448 if field != "id" && !collection.fields.contains_key(field) {
449 ctx.add_error(
450 ErrorCode::UnknownField,
451 format!(
452 "Filter field '{}' does not exist in collection '{}'",
453 field, collection.name
454 ),
455 );
456 }
457}
458
459fn validate_filter_field<S: SchemaProvider>(
461 field: &str,
462 value: &Value,
463 collection: &Collection,
464 ctx: &mut ValidationContext<'_, S>,
465) {
466 if field == "id" {
467 return; }
469
470 if let Some(field_def) = collection.fields.get(field) {
471 if !is_type_compatible(&field_def.field_type, value) {
473 ctx.add_error(
474 ErrorCode::TypeMismatch,
475 format!(
476 "Type mismatch: field '{}' expects {:?}, got {:?}",
477 field,
478 field_def.field_type,
479 value_type_name(value)
480 ),
481 );
482 }
483 } else {
484 ctx.add_error(
485 ErrorCode::UnknownField,
486 format!(
487 "Filter field '{}' does not exist in collection '{}'",
488 field, collection.name
489 ),
490 );
491 }
492}
493
494fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
496 match (field_type, value) {
497 (_, Value::Null) => true, (_, Value::Variable(_)) => true, (FieldType::String, Value::String(_)) => true,
500 (FieldType::Int, Value::Int(_)) => true,
501 (FieldType::Float, Value::Float(_)) => true,
502 (FieldType::Float, Value::Int(_)) => true, (FieldType::Bool, Value::Boolean(_)) => true,
504 (FieldType::Array, Value::Array(_)) => true,
505 (FieldType::Object, Value::Object(_)) => true,
506 (FieldType::Any, _) => true, (FieldType::Uuid, Value::String(_)) => true, _ => false,
509 }
510}
511
512fn value_type_name(value: &Value) -> &'static str {
514 match value {
515 Value::Null => "null",
516 Value::Boolean(_) => "boolean",
517 Value::Int(_) => "int",
518 Value::Float(_) => "float",
519 Value::String(_) => "string",
520 Value::Array(_) => "array",
521 Value::Object(_) => "object",
522 Value::Variable(_) => "variable",
523 Value::Enum(_) => "enum",
524 }
525}
526
527fn extract_filter_from_value(value: &Value) -> Option<Filter> {
529 match value {
530 Value::Object(map) => {
531 let mut filters = Vec::new();
532
533 for (key, val) in map {
534 match key.as_str() {
535 "and" => {
536 if let Value::Array(arr) = val {
537 let sub_filters: Vec<Filter> =
538 arr.iter().filter_map(extract_filter_from_value).collect();
539 if !sub_filters.is_empty() {
540 filters.push(Filter::And(sub_filters));
541 }
542 }
543 }
544 "or" => {
545 if let Value::Array(arr) = val {
546 let sub_filters: Vec<Filter> =
547 arr.iter().filter_map(extract_filter_from_value).collect();
548 if !sub_filters.is_empty() {
549 filters.push(Filter::Or(sub_filters));
550 }
551 }
552 }
553 "not" => {
554 if let Some(inner) = extract_filter_from_value(val) {
555 filters.push(Filter::Not(Box::new(inner)));
556 }
557 }
558 field => {
559 if let Value::Object(ops) = val {
561 for (op, op_val) in ops {
562 let filter = match op.as_str() {
563 "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
564 "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
565 "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
566 "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
567 "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
568 "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
569 "in" => Some(Filter::In(field.to_string(), op_val.clone())),
570 "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
571 "contains" => {
572 Some(Filter::Contains(field.to_string(), op_val.clone()))
573 }
574 "startsWith" => {
575 Some(Filter::StartsWith(field.to_string(), op_val.clone()))
576 }
577 "endsWith" => {
578 Some(Filter::EndsWith(field.to_string(), op_val.clone()))
579 }
580 "isNull" => Some(Filter::IsNull(field.to_string())),
581 "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
582 _ => None,
583 };
584 if let Some(f) = filter {
585 filters.push(f);
586 }
587 }
588 }
589 }
590 }
591 }
592
593 match filters.len() {
594 0 => None,
595 1 => Some(filters.remove(0)),
596 _ => Some(Filter::And(filters)),
597 }
598 }
599 _ => None,
600 }
601}
602
603pub fn resolve_variables(
605 doc: &mut Document,
606 variables: &HashMap<String, ast::Value>,
607) -> Result<(), ValidationError> {
608 for op in &mut doc.operations {
609 match op {
610 ast::Operation::Query(query) => {
611 resolve_in_fields(&mut query.selection_set, variables)?;
612 }
613 ast::Operation::Mutation(mutation) => {
614 for mut_op in &mut mutation.operations {
615 resolve_in_mutation_op(mut_op, variables)?;
616 }
617 }
618 ast::Operation::Subscription(sub) => {
619 resolve_in_fields(&mut sub.selection_set, variables)?;
620 }
621 ast::Operation::Schema(_) => {}
622 ast::Operation::Migration(_) => {}
623 ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
627 }
628 Ok(())
629}
630
631fn resolve_in_fields(
632 fields: &mut [Field],
633 variables: &HashMap<String, ast::Value>,
634) -> Result<(), ValidationError> {
635 for field in fields {
636 for arg in &mut field.arguments {
638 resolve_in_value(&mut arg.value, variables)?;
639 }
640 resolve_in_fields(&mut field.selection_set, variables)?;
642 }
643 Ok(())
644}
645
646fn resolve_in_mutation_op(
647 op: &mut ast::MutationOperation,
648 variables: &HashMap<String, ast::Value>,
649) -> Result<(), ValidationError> {
650 match &mut op.operation {
651 ast::MutationOp::Insert { data, .. }
652 | ast::MutationOp::Update { data, .. }
653 | ast::MutationOp::Upsert { data, .. } => {
654 resolve_in_value(data, variables)?;
655 }
656 ast::MutationOp::InsertMany { data, .. } => {
657 for item in data {
658 resolve_in_value(item, variables)?;
659 }
660 }
661 ast::MutationOp::Delete { .. } => {}
662 ast::MutationOp::EnqueueJob { payload, .. } => {
663 resolve_in_value(payload, variables)?;
664 }
665 ast::MutationOp::Transaction { operations } => {
666 for inner in operations {
667 resolve_in_mutation_op(inner, variables)?;
668 }
669 }
670 }
671 resolve_in_fields(&mut op.selection_set, variables)
672}
673
674fn resolve_in_value(
675 value: &mut Value,
676 variables: &HashMap<String, ast::Value>,
677) -> Result<(), ValidationError> {
678 match value {
679 Value::Variable(name) => {
680 if let Some(resolved) = variables.get(name) {
681 *value = resolved.clone();
682 } else {
683 return Err(ValidationError::new(
684 ErrorCode::MissingRequiredVariable,
685 format!("Variable '{}' is not provided", name),
686 ));
687 }
688 }
689 Value::Array(items) => {
690 for item in items {
691 resolve_in_value(item, variables)?;
692 }
693 }
694 Value::Object(map) => {
695 for v in map.values_mut() {
696 resolve_in_value(v, variables)?;
697 }
698 }
699 _ => {}
700 }
701 Ok(())
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use crate::types::FieldDefinition;
708
709 fn create_test_schema() -> InMemorySchema {
710 let mut schema = InMemorySchema::new();
711
712 let mut users_fields = HashMap::new();
713 users_fields.insert(
714 "name".to_string(),
715 FieldDefinition {
716 field_type: FieldType::String,
717 unique: false,
718 indexed: false,
719 },
720 );
721 users_fields.insert(
722 "email".to_string(),
723 FieldDefinition {
724 field_type: FieldType::String,
725 unique: true,
726 indexed: true,
727 },
728 );
729 users_fields.insert(
730 "age".to_string(),
731 FieldDefinition {
732 field_type: FieldType::Int,
733 unique: false,
734 indexed: false,
735 },
736 );
737 users_fields.insert(
738 "active".to_string(),
739 FieldDefinition {
740 field_type: FieldType::Bool,
741 unique: false,
742 indexed: false,
743 },
744 );
745
746 schema.add_collection(Collection {
747 name: "users".to_string(),
748 fields: users_fields,
749 });
750
751 schema
752 }
753
754 #[test]
755 fn test_validate_unknown_collection() {
756 let schema = create_test_schema();
757 let doc = Document {
758 operations: vec![ast::Operation::Query(Query {
759 name: None,
760 variable_definitions: vec![],
761 directives: vec![],
762 selection_set: vec![Field {
763 alias: None,
764 name: "nonexistent".to_string(),
765 arguments: vec![],
766 directives: vec![],
767 selection_set: vec![Field {
768 alias: None,
769 name: "id".to_string(),
770 arguments: vec![],
771 directives: vec![],
772 selection_set: vec![],
773 }],
774 }],
775 variables_values: HashMap::new(),
776 })],
777 };
778
779 let result = validate_document(&doc, &schema, HashMap::new());
780 assert!(result.is_err());
781 let errors = result.unwrap_err();
782 assert!(
783 errors
784 .iter()
785 .any(|e| e.code == ErrorCode::UnknownCollection)
786 );
787 }
788
789 #[test]
790 fn test_validate_unknown_field() {
791 let schema = create_test_schema();
792 let doc = Document {
793 operations: vec![ast::Operation::Query(Query {
794 name: None,
795 variable_definitions: vec![],
796 directives: vec![],
797 selection_set: vec![Field {
798 alias: None,
799 name: "users".to_string(),
800 arguments: vec![],
801 directives: vec![],
802 selection_set: vec![Field {
803 alias: None,
804 name: "nonexistent_field".to_string(),
805 arguments: vec![],
806 directives: vec![],
807 selection_set: vec![],
808 }],
809 }],
810 variables_values: HashMap::new(),
811 })],
812 };
813
814 let result = validate_document(&doc, &schema, HashMap::new());
815 assert!(result.is_err());
816 let errors = result.unwrap_err();
817 assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
818 }
819
820 #[test]
821 fn test_validate_missing_required_variable() {
822 let schema = create_test_schema();
823 let doc = Document {
824 operations: vec![ast::Operation::Query(Query {
825 name: Some("GetUsers".to_string()),
826 variable_definitions: vec![ast::VariableDefinition {
827 name: "minAge".to_string(),
828 var_type: ast::TypeAnnotation {
829 name: "Int".to_string(),
830 is_array: false,
831 is_required: true,
832 },
833 default_value: None,
834 }],
835 directives: vec![],
836 selection_set: vec![],
837 variables_values: HashMap::new(),
838 })],
839 };
840
841 let result = validate_document(&doc, &schema, HashMap::new());
843 assert!(result.is_err());
844 let errors = result.unwrap_err();
845 assert!(
846 errors
847 .iter()
848 .any(|e| e.code == ErrorCode::MissingRequiredVariable)
849 );
850 }
851
852 #[test]
853 fn test_validate_valid_query() {
854 let schema = create_test_schema();
855 let doc = Document {
856 operations: vec![ast::Operation::Query(Query {
857 name: Some("GetUsers".to_string()),
858 variable_definitions: vec![],
859 directives: vec![],
860 selection_set: vec![Field {
861 alias: None,
862 name: "users".to_string(),
863 arguments: vec![],
864 directives: vec![],
865 selection_set: vec![
866 Field {
867 alias: None,
868 name: "id".to_string(),
869 arguments: vec![],
870 directives: vec![],
871 selection_set: vec![],
872 },
873 Field {
874 alias: None,
875 name: "name".to_string(),
876 arguments: vec![],
877 directives: vec![],
878 selection_set: vec![],
879 },
880 Field {
881 alias: None,
882 name: "email".to_string(),
883 arguments: vec![],
884 directives: vec![],
885 selection_set: vec![],
886 },
887 ],
888 }],
889 variables_values: HashMap::new(),
890 })],
891 };
892
893 let result = validate_document(&doc, &schema, HashMap::new());
894 assert!(result.is_ok());
895 }
896
897 #[test]
898 fn test_validate_filter_type_mismatch() {
899 let schema = create_test_schema();
900 let collection = schema.get_collection("users").unwrap();
901 let mut ctx = ValidationContext::new(&schema);
902
903 let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
905 validate_filter(&filter, collection, &mut ctx);
906
907 assert!(ctx.has_errors());
908 assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
909 }
910
911 #[test]
912 fn test_validate_filter_string_operator_on_int() {
913 let schema = create_test_schema();
914 let collection = schema.get_collection("users").unwrap();
915 let mut ctx = ValidationContext::new(&schema);
916
917 let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
919 validate_filter(&filter, collection, &mut ctx);
920
921 assert!(ctx.has_errors());
922 assert!(
923 ctx.errors
924 .iter()
925 .any(|e| e.code == ErrorCode::InvalidFilterOperator)
926 );
927 }
928
929 #[test]
930 fn test_resolve_variables() {
931 let mut doc = Document {
932 operations: vec![ast::Operation::Query(Query {
933 name: None,
934 variable_definitions: vec![],
935 directives: vec![],
936 selection_set: vec![Field {
937 alias: None,
938 name: "users".to_string(),
939 arguments: vec![ast::Argument {
940 name: "limit".to_string(),
941 value: Value::Variable("pageSize".to_string()),
942 }],
943 directives: vec![],
944 selection_set: vec![],
945 }],
946 variables_values: HashMap::new(),
947 })],
948 };
949
950 let mut vars = HashMap::new();
951 vars.insert("pageSize".to_string(), Value::Int(10));
952
953 let result = resolve_variables(&mut doc, &vars);
954 assert!(result.is_ok());
955
956 if let ast::Operation::Query(query) = &doc.operations[0] {
958 let arg = &query.selection_set[0].arguments[0];
959 assert!(matches!(arg.value, Value::Int(10)));
960 } else {
961 panic!("Expected Query operation");
962 }
963 }
964}