1use super::ast::{
10 self, Document, Field, Filter, FragmentDef, Mutation, Query, Selection, Subscription, Value,
11};
12use crate::types::{Collection, FieldType, ScalarType};
13use std::collections::{HashMap, HashSet};
14use std::fmt;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorCode {
19 UnknownCollection,
21 UnknownField,
23 InvalidInput,
25 TypeMismatch,
27 MissingRequiredVariable,
29 MissingOptionalVariable,
31 InvalidFilterOperator,
33 DuplicateAlias,
35 InvalidArgument,
37 UnknownDirective,
39 UnknownFragment,
41}
42
43impl fmt::Display for ErrorCode {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 ErrorCode::UnknownCollection => write!(f, "UnknownCollection"),
47 ErrorCode::UnknownField => write!(f, "UnknownField"),
48 ErrorCode::InvalidInput => write!(f, "InvalidInput"),
49 ErrorCode::TypeMismatch => write!(f, "TypeMismatch"),
50 ErrorCode::MissingRequiredVariable => write!(f, "MissingRequiredVariable"),
51 ErrorCode::MissingOptionalVariable => write!(f, "MissingOptionalVariable"),
52 ErrorCode::InvalidFilterOperator => write!(f, "InvalidFilterOperator"),
53 ErrorCode::DuplicateAlias => write!(f, "DuplicateAlias"),
54 ErrorCode::InvalidArgument => write!(f, "InvalidArgument"),
55 ErrorCode::UnknownDirective => write!(f, "UnknownDirective"),
56 ErrorCode::UnknownFragment => write!(f, "UnknownFragment"),
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct ValidationError {
64 pub code: ErrorCode,
65 pub message: String,
66 pub path: Option<String>,
67}
68
69impl fmt::Display for ValidationError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match &self.path {
72 Some(path) => write!(f, "[{}] {}", path, self.message),
73 None => write!(f, "{}", self.message),
74 }
75 }
76}
77
78impl std::error::Error for ValidationError {}
79
80impl ValidationError {
81 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
82 Self {
83 code,
84 message: message.into(),
85 path: None,
86 }
87 }
88
89 pub fn with_path(mut self, path: impl Into<String>) -> Self {
90 self.path = Some(path.into());
91 self
92 }
93}
94
95pub type ValidationResult = Result<(), Vec<ValidationError>>;
97
98pub trait SchemaProvider {
101 fn get_collection(&self, name: &str) -> Option<&Collection>;
103
104 fn collection_exists(&self, name: &str) -> bool {
106 self.get_collection(name).is_some()
107 }
108}
109
110#[derive(Debug, Default)]
112pub struct InMemorySchema {
113 collections: HashMap<String, Collection>,
114}
115
116impl InMemorySchema {
117 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn add_collection(&mut self, collection: Collection) {
122 self.collections.insert(collection.name.clone(), collection);
123 }
124}
125
126impl SchemaProvider for InMemorySchema {
127 fn get_collection(&self, name: &str) -> Option<&Collection> {
128 self.collections.get(name)
129 }
130}
131
132pub struct ValidationContext<'a, S: SchemaProvider> {
134 pub schema: &'a S,
135 pub variables: HashMap<String, ast::Value>,
136 pub fragments: HashMap<String, &'a FragmentDef>,
137 pub errors: Vec<ValidationError>,
138 current_path: Vec<String>,
139 validating_fragments: HashSet<String>,
140}
141
142impl<'a, S: SchemaProvider> ValidationContext<'a, S> {
143 pub fn new(schema: &'a S) -> Self {
144 Self {
145 schema,
146 variables: HashMap::new(),
147 fragments: HashMap::new(),
148 errors: Vec::new(),
149 current_path: Vec::new(),
150 validating_fragments: HashSet::new(),
151 }
152 }
153
154 pub fn with_variables(mut self, variables: HashMap<String, ast::Value>) -> Self {
155 self.variables = variables;
156 self
157 }
158
159 fn push_path(&mut self, segment: &str) {
160 self.current_path.push(segment.to_string());
161 }
162
163 fn pop_path(&mut self) {
164 self.current_path.pop();
165 }
166
167 fn current_path_string(&self) -> Option<String> {
168 if self.current_path.is_empty() {
169 None
170 } else {
171 Some(self.current_path.join("."))
172 }
173 }
174
175 fn add_error(&mut self, code: ErrorCode, message: impl Into<String>) {
176 let mut error = ValidationError::new(code, message);
177 error.path = self.current_path_string();
178 self.errors.push(error);
179 }
180
181 pub fn has_errors(&self) -> bool {
182 !self.errors.is_empty()
183 }
184
185 pub fn into_result(self) -> ValidationResult {
186 if self.errors.is_empty() {
187 Ok(())
188 } else {
189 Err(self.errors)
190 }
191 }
192}
193
194pub fn validate_document<S: SchemaProvider>(
196 doc: &Document,
197 schema: &S,
198 variables: HashMap<String, ast::Value>,
199) -> ValidationResult {
200 let mut ctx = ValidationContext::new(schema).with_variables(variables);
201
202 for op in &doc.operations {
204 if let ast::Operation::FragmentDefinition(frag) = op {
205 if ctx.fragments.insert(frag.name.clone(), frag).is_some() {
206 ctx.add_error(
207 ErrorCode::InvalidInput,
208 format!("Duplicate fragment definition '{}'", frag.name),
209 );
210 }
211 }
212 }
213
214 for (i, op) in doc.operations.iter().enumerate() {
215 ctx.push_path(&format!("operation[{}]", i));
216 match op {
217 ast::Operation::Query(query) => validate_query(query, &mut ctx),
218 ast::Operation::Mutation(mutation) => validate_mutation(mutation, &mut ctx),
219 ast::Operation::Subscription(sub) => validate_subscription(sub, &mut ctx),
220 ast::Operation::Schema(_) => {} ast::Operation::Migration(_) => {}
222 ast::Operation::FragmentDefinition(_) => {} ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
226 ctx.pop_path();
227 }
228
229 ctx.into_result()
230}
231
232fn validate_query<S: SchemaProvider>(query: &Query, ctx: &mut ValidationContext<'_, S>) {
234 if let Some(name) = &query.name {
235 ctx.push_path(name);
236 }
237
238 validate_variable_definitions(&query.variable_definitions, ctx);
240
241 for selection in &query.selection_set {
243 match selection {
244 Selection::Field(field) => {
245 validate_field(field, None, ctx);
246 }
247 Selection::InlineFragment(inline) => {
248 validate_inline_fragment(inline, None, ctx);
249 }
250 Selection::FragmentSpread(fragment_name) => {
251 ctx.add_error(
254 ErrorCode::InvalidArgument,
255 format!(
256 "Fragment spread '{}' is not allowed at query root; \
257 use it inside a collection selection set instead",
258 fragment_name
259 ),
260 );
261 }
262 }
263 }
264
265 if query.name.is_some() {
266 ctx.pop_path();
267 }
268}
269
270fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
272 if let Some(name) = &mutation.name {
273 ctx.push_path(name);
274 }
275
276 validate_variable_definitions(&mutation.variable_definitions, ctx);
278
279 for (i, op) in mutation.operations.iter().enumerate() {
281 ctx.push_path(&format!("mutation[{}]", i));
282 validate_mutation_operation(op, ctx);
283 ctx.pop_path();
284 }
285
286 if mutation.name.is_some() {
287 ctx.pop_path();
288 }
289}
290
291fn validate_inline_fragment<S: SchemaProvider>(
297 inline: &ast::InlineFragment,
298 expected_collection: Option<&str>,
299 ctx: &mut ValidationContext<'_, S>,
300) {
301 if let Some(enc) = expected_collection {
303 if inline.type_condition != enc {
304 ctx.add_error(
305 ErrorCode::TypeMismatch,
306 format!(
307 "Inline fragment on '{}' cannot appear inside '{}'",
308 inline.type_condition, enc
309 ),
310 );
311 return;
312 }
313 }
314
315 if !ctx.schema.collection_exists(&inline.type_condition) {
317 ctx.add_error(
318 ErrorCode::UnknownCollection,
319 format!(
320 "Unknown collection '{}' in inline fragment",
321 inline.type_condition
322 ),
323 );
324 return;
325 }
326
327 if let Some(collection) = ctx.schema.get_collection(&inline.type_condition) {
329 validate_selection_set(&inline.selection_set, collection, ctx);
330 }
331}
332
333fn validate_subscription<S: SchemaProvider>(
335 sub: &Subscription,
336 ctx: &mut ValidationContext<'_, S>,
337) {
338 if let Some(name) = &sub.name {
339 ctx.push_path(name);
340 }
341
342 validate_variable_definitions(&sub.variable_definitions, ctx);
344
345 for selection in &sub.selection_set {
347 match selection {
348 Selection::Field(field) => {
349 validate_field(field, None, ctx);
350 }
351 Selection::InlineFragment(inline) => {
352 validate_inline_fragment(inline, None, ctx);
353 }
354 Selection::FragmentSpread(fragment_name) => {
355 if let Some(fragment) = ctx.fragments.get(fragment_name) {
356 validate_fragment_spread(fragment_name, &fragment.type_condition, ctx);
357 } else {
358 ctx.add_error(
359 ErrorCode::UnknownFragment,
360 format!("Fragment '{}' is not defined", fragment_name),
361 );
362 }
363 }
364 }
365 }
366
367 if sub.name.is_some() {
368 ctx.pop_path();
369 }
370}
371
372fn validate_variable_definitions<S: SchemaProvider>(
374 definitions: &[ast::VariableDefinition],
375 ctx: &mut ValidationContext<'_, S>,
376) {
377 for def in definitions {
378 let var_name = &def.name;
379
380 if def.var_type.is_required && def.default_value.is_none() {
382 if !ctx.variables.contains_key(var_name) {
383 ctx.add_error(
384 ErrorCode::MissingRequiredVariable,
385 format!("Required variable '{}' is not provided", var_name),
386 );
387 }
388 }
389 }
390}
391
392fn validate_field<S: SchemaProvider>(
397 field: &Field,
398 parent_collection: Option<&Collection>,
399 ctx: &mut ValidationContext<'_, S>,
400) {
401 let field_name = field.alias.as_ref().unwrap_or(&field.name);
402 ctx.push_path(field_name);
403
404 match parent_collection {
405 None => {
406 let collection_name = &field.name;
408 if !ctx.schema.collection_exists(collection_name) {
409 ctx.add_error(
410 ErrorCode::UnknownCollection,
411 format!("Collection '{}' does not exist", collection_name),
412 );
413 } else if field.selection_set.is_empty() {
414 ctx.add_error(
415 ErrorCode::InvalidInput,
416 format!("Collection '{}' requires a selection set", collection_name),
417 );
418 } else if let Some(collection) = ctx.schema.get_collection(collection_name) {
419 validate_selection_set(&field.selection_set, collection, ctx);
421
422 for arg in &field.arguments {
424 if arg.name == "where" || arg.name == "filter" {
425 report_unknown_filter_ops(&arg.value, ctx);
426 if let Some(filter) = extract_filter_from_value(&arg.value) {
427 validate_filter(&filter, collection, ctx);
428 }
429 }
430 }
431 }
432 }
433 Some(collection) => {
434 if !field.selection_set.is_empty() {
436 if let Some(field_def) = collection.fields.get(&field.name) {
437 match &field_def.field_type {
438 FieldType::Nested(nested_schema) => {
439 validate_nested_selection_set(&field.selection_set, nested_schema, ctx);
441 }
442 FieldType::Object | FieldType::Any => {
443 }
446 FieldType::Scalar(ScalarType::Object)
447 | FieldType::Scalar(ScalarType::Any) => {
448 }
450 _ => {
451 ctx.add_error(
453 ErrorCode::TypeMismatch,
454 format!(
455 "Field '{}' is not an object type but has a selection set",
456 field.name
457 ),
458 );
459 }
460 }
461 }
462 }
464 }
465 }
466
467 ctx.pop_path();
468}
469
470fn validate_selection_set<S: SchemaProvider>(
472 selections: &[Selection],
473 collection: &Collection,
474 ctx: &mut ValidationContext<'_, S>,
475) {
476 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
477
478 for selection in selections {
479 match selection {
481 Selection::Field(field) => {
482 let display_name = field.alias.as_ref().unwrap_or(&field.name);
483
484 if aliases_seen.contains_key(display_name) {
486 ctx.add_error(
487 ErrorCode::DuplicateAlias,
488 format!("Duplicate field/alias '{}' in selection", display_name),
489 );
490 }
491 aliases_seen.insert(display_name.clone(), true);
492
493 let field_name = &field.name;
495 if !field_name.starts_with("__") && field_name != "id" {
496 if !collection.fields.contains_key(field_name) {
497 ctx.add_error(
498 ErrorCode::UnknownField,
499 format!(
500 "Field '{}' does not exist in collection '{}'",
501 field_name, collection.name
502 ),
503 );
504 }
505 }
506
507 validate_field(field, Some(collection), ctx);
509 }
510 Selection::InlineFragment(inline) => {
511 validate_inline_fragment(inline, Some(&collection.name), ctx);
512 }
513 Selection::FragmentSpread(fragment_name) => {
514 validate_fragment_spread(fragment_name, &collection.name, ctx);
515 }
516 }
517 }
518}
519
520fn validate_nested_selection_set<S: SchemaProvider>(
522 selections: &[Selection],
523 nested_schema: &HashMap<String, crate::types::FieldDefinition>,
524 ctx: &mut ValidationContext<'_, S>,
525) {
526 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
527
528 for selection in selections {
529 match selection {
530 Selection::Field(field) => {
531 let display_name = field.alias.as_ref().unwrap_or(&field.name);
532
533 if aliases_seen.contains_key(display_name) {
535 ctx.add_error(
536 ErrorCode::DuplicateAlias,
537 format!("Duplicate field/alias '{}' in selection", display_name),
538 );
539 }
540 aliases_seen.insert(display_name.clone(), true);
541
542 let field_name = &field.name;
544 if !field_name.starts_with("__") {
545 if !nested_schema.contains_key(field_name) {
546 ctx.add_error(
547 ErrorCode::UnknownField,
548 format!("Field '{}' does not exist in nested object", field_name),
549 );
550 } else if !field.selection_set.is_empty() {
551 if let Some(field_def) = nested_schema.get(field_name) {
553 match &field_def.field_type {
554 FieldType::Nested(deeper_schema) => {
555 ctx.push_path(field_name);
556 validate_nested_selection_set(
557 &field.selection_set,
558 deeper_schema,
559 ctx,
560 );
561 ctx.pop_path();
562 }
563 FieldType::Object | FieldType::Any => {
564 }
566 FieldType::Scalar(ScalarType::Object)
567 | FieldType::Scalar(ScalarType::Any) => {
568 }
570 _ => {
571 ctx.add_error(
572 ErrorCode::TypeMismatch,
573 format!(
574 "Field '{}' is not an object type but has a selection set",
575 field_name
576 ),
577 );
578 }
579 }
580 }
581 }
582 }
583 }
584 Selection::InlineFragment(_) | Selection::FragmentSpread(_) => {
585 ctx.add_error(
587 ErrorCode::InvalidInput,
588 "Fragments are not supported in nested object selections".to_string(),
589 );
590 }
591 }
592 }
593}
594
595fn validate_fragment_spread<S: SchemaProvider>(
597 fragment_name: &str,
598 expected_collection: &str,
599 ctx: &mut ValidationContext<'_, S>,
600) {
601 if ctx.validating_fragments.contains(fragment_name) {
603 ctx.add_error(
604 ErrorCode::InvalidInput,
605 format!("Fragment '{}' contains a cyclic reference", fragment_name),
606 );
607 return;
608 }
609
610 let fragment_opt = ctx.fragments.get(fragment_name).cloned();
612
613 if let Some(fragment) = fragment_opt {
614 if fragment.type_condition != expected_collection {
616 ctx.add_error(
617 ErrorCode::TypeMismatch,
618 format!(
619 "Fragment '{}' is defined on '{}' but used on '{}'",
620 fragment_name, fragment.type_condition, expected_collection
621 ),
622 );
623 return;
624 }
625
626 if let Some(collection) = ctx.schema.get_collection(expected_collection) {
628 ctx.push_path(&format!("...{}", fragment_name));
629 ctx.validating_fragments.insert(fragment_name.to_string());
630 validate_selection_set(&fragment.selection_set, collection, ctx);
631 ctx.validating_fragments.remove(fragment_name);
632 ctx.pop_path();
633 } else {
634 ctx.add_error(
635 ErrorCode::UnknownCollection,
636 format!(
637 "Fragment '{}' references unknown collection '{}'",
638 fragment_name, expected_collection
639 ),
640 );
641 }
642 } else {
643 ctx.add_error(
644 ErrorCode::UnknownFragment,
645 format!("Fragment '{}' is not defined", fragment_name),
646 );
647 }
648}
649
650fn validate_mutation_operation<S: SchemaProvider>(
652 op: &ast::MutationOperation,
653 ctx: &mut ValidationContext<'_, S>,
654) {
655 match &op.operation {
656 ast::MutationOp::Insert { collection, data } => {
657 if let Some(col_def) = ctx.schema.get_collection(collection) {
658 validate_object_against_schema(data, col_def, ctx);
659 } else {
660 ctx.add_error(
661 ErrorCode::UnknownCollection,
662 format!("Collection '{}' does not exist", collection),
663 );
664 }
665 }
666 ast::MutationOp::InsertMany { collection, data } => {
667 if let Some(col_def) = ctx.schema.get_collection(collection) {
668 for item in data {
669 validate_object_against_schema(item, col_def, ctx);
670 }
671 } else {
672 ctx.add_error(
673 ErrorCode::UnknownCollection,
674 format!("Collection '{}' does not exist", collection),
675 );
676 }
677 }
678 ast::MutationOp::Update {
679 collection, data, ..
680 }
681 | ast::MutationOp::Upsert {
682 collection, data, ..
683 } => {
684 if let Some(col_def) = ctx.schema.get_collection(collection) {
685 validate_partial_object(data, col_def, ctx);
687 } else {
688 ctx.add_error(
689 ErrorCode::UnknownCollection,
690 format!("Collection '{}' does not exist", collection),
691 );
692 }
693 }
694 ast::MutationOp::Delete { collection, .. } => {
695 if !ctx.schema.collection_exists(collection) {
696 ctx.add_error(
697 ErrorCode::UnknownCollection,
698 format!("Collection '{}' does not exist", collection),
699 );
700 }
701 }
702 ast::MutationOp::EnqueueJob { .. } => {}
703 ast::MutationOp::EnqueueJobs { .. } => {}
704 ast::MutationOp::Import { collection, data } => {
705 if let Some(col_def) = ctx.schema.get_collection(collection) {
706 for item in data {
707 validate_object_against_schema(item, col_def, ctx);
708 }
709 }
710 }
711 ast::MutationOp::Export { .. } => {}
712 ast::MutationOp::Transaction { operations } => {
713 for (i, inner_op) in operations.iter().enumerate() {
714 ctx.push_path(&format!("tx[{}]", i));
715 validate_mutation_operation(inner_op, ctx);
716 ctx.pop_path();
717 }
718 }
719 }
720}
721
722fn validate_object_against_schema<S: SchemaProvider>(
724 value: &Value,
725 collection: &Collection,
726 ctx: &mut ValidationContext<'_, S>,
727) {
728 match value {
729 Value::Object(map) => {
730 for (key, val) in map {
732 if let Some(field_def) = collection.fields.get(key) {
733 if matches!(val, Value::Null) && !field_def.nullable {
735 ctx.add_error(
736 ErrorCode::InvalidInput,
737 format!("Field '{}' cannot be null", key),
738 );
739 } else if !matches!(val, Value::Null)
740 && !validate_value_against_type(val, &field_def.field_type)
741 {
742 ctx.add_error(
743 ErrorCode::TypeMismatch,
744 format!(
745 "Type mismatch for field '{}': expected {:?}, got {:?}",
746 key,
747 field_def.field_type,
748 value_type_name(val)
749 ),
750 );
751 }
752 } else if key != "id" {
753 ctx.add_error(
754 ErrorCode::UnknownField,
755 format!(
756 "Field '{}' not defined in collection '{}'",
757 key, collection.name
758 ),
759 );
760 }
761 }
762
763 for (name, def) in &collection.fields {
765 if !def.nullable && !map.contains_key(name) {
766 ctx.add_error(
767 ErrorCode::InvalidInput,
768 format!("Missing required field '{}'", name),
769 );
770 }
771 }
772 }
773 _ => {
774 ctx.add_error(
775 ErrorCode::TypeMismatch,
776 format!(
777 "Expected object for collection '{}', got {:?}",
778 collection.name,
779 value_type_name(value)
780 ),
781 );
782 }
783 }
784}
785
786fn validate_partial_object<S: SchemaProvider>(
789 value: &Value,
790 collection: &Collection,
791 ctx: &mut ValidationContext<'_, S>,
792) {
793 match value {
794 Value::Object(map) => {
795 for (key, val) in map {
797 if let Some(field_def) = collection.fields.get(key) {
798 if matches!(val, Value::Null) && !field_def.nullable {
800 ctx.add_error(
801 ErrorCode::InvalidInput,
802 format!("Field '{}' cannot be null", key),
803 );
804 } else if !matches!(val, Value::Null)
805 && !validate_value_against_type(val, &field_def.field_type)
806 {
807 ctx.add_error(
808 ErrorCode::TypeMismatch,
809 format!(
810 "Type mismatch for field '{}': expected {:?}, got {:?}",
811 key,
812 field_def.field_type,
813 value_type_name(val)
814 ),
815 );
816 }
817 } else if key != "id" {
818 ctx.add_error(
819 ErrorCode::UnknownField,
820 format!(
821 "Field '{}' not defined in collection '{}'",
822 key, collection.name
823 ),
824 );
825 }
826 }
827 }
828 _ => {
829 ctx.add_error(
830 ErrorCode::TypeMismatch,
831 format!(
832 "Expected object for collection '{}', got {:?}",
833 collection.name,
834 value_type_name(value)
835 ),
836 );
837 }
838 }
839}
840
841fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
843 use crate::types::ScalarType;
844 match (expected, value) {
845 (FieldType::Scalar(ScalarType::Any), _) => true,
847 (_, Value::Null) => true,
848 (_, Value::Variable(_)) => true,
849
850 (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
851 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
852 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
853 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
854 (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
855 (FieldType::Scalar(ScalarType::Uuid), Value::String(_)) => true,
856
857 (FieldType::Array(scalar_inner), Value::Array(items)) => {
859 let inner_field_type = FieldType::Scalar(scalar_inner.clone());
861 items
862 .iter()
863 .all(|item| validate_value_against_type(item, &inner_field_type))
864 }
865
866 (FieldType::Object, Value::Object(_)) => true,
868 (FieldType::Nested(schema), Value::Object(map)) => {
870 for (key, val) in map {
872 if let Some(def) = schema.get(key) {
873 if matches!(val, Value::Null) {
874 if !def.nullable {
875 return false; }
877 } else if !validate_value_against_type(val, &def.field_type) {
878 return false; }
880 } else {
881 return false; }
883 }
884 for (key, def) in schema.iter() {
886 if !def.nullable && !map.contains_key(key) {
887 return false; }
889 }
890 true
891 }
892 (FieldType::Nested(_), _) => false, (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
896 (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
898
899 _ => false,
900 }
901}
902
903fn validate_filter<S: SchemaProvider>(
905 filter: &Filter,
906 collection: &Collection,
907 ctx: &mut ValidationContext<'_, S>,
908) {
909 match filter {
910 Filter::Eq(field, value)
911 | Filter::Ne(field, value)
912 | Filter::Gt(field, value)
913 | Filter::Gte(field, value)
914 | Filter::Lt(field, value)
915 | Filter::Lte(field, value) => {
916 validate_filter_field(field, value, collection, ctx);
917 }
918 Filter::In(field, value) | Filter::NotIn(field, value) => {
919 if !matches!(value, Value::Array(_)) {
921 ctx.add_error(
922 ErrorCode::TypeMismatch,
923 format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
924 );
925 }
926 validate_filter_field_exists(field, collection, ctx);
927 }
928 Filter::ContainsAny(field, value) | Filter::ContainsAll(field, value) => {
929 if !matches!(value, Value::Array(_) | Value::Variable(_)) {
930 ctx.add_error(
931 ErrorCode::TypeMismatch,
932 format!("containsAny/containsAll on field '{}' expects an array", field),
933 );
934 }
935 validate_filter_field_exists(field, collection, ctx);
936 }
937 Filter::Contains(field, _)
938 | Filter::StartsWith(field, _)
939 | Filter::EndsWith(field, _)
940 | Filter::Matches(field, _) => {
941 if let Some(field_def) = collection.fields.get(field) {
943 if field_def.field_type != FieldType::SCALAR_STRING {
944 ctx.add_error(
945 ErrorCode::InvalidFilterOperator,
946 format!("String operator on non-string field '{}'", field),
947 );
948 }
949 } else {
950 validate_filter_field_exists(field, collection, ctx);
951 }
952 }
953 Filter::IsNull(field) | Filter::IsNotNull(field) => {
954 validate_filter_field_exists(field, collection, ctx);
955 }
956 Filter::And(filters) | Filter::Or(filters) => {
957 for f in filters {
958 validate_filter(f, collection, ctx);
959 }
960 }
961 Filter::Not(inner) => {
962 validate_filter(inner, collection, ctx);
963 }
964 }
965}
966
967fn validate_filter_field_exists<S: SchemaProvider>(
969 field: &str,
970 collection: &Collection,
971 ctx: &mut ValidationContext<'_, S>,
972) {
973 if field != "id" && !collection.fields.contains_key(field) {
974 ctx.add_error(
975 ErrorCode::UnknownField,
976 format!(
977 "Filter field '{}' does not exist in collection '{}'",
978 field, collection.name
979 ),
980 );
981 }
982}
983
984fn validate_filter_field<S: SchemaProvider>(
986 field: &str,
987 value: &Value,
988 collection: &Collection,
989 ctx: &mut ValidationContext<'_, S>,
990) {
991 if field == "id" {
992 return; }
994
995 if let Some(field_def) = collection.fields.get(field) {
996 if !is_type_compatible(&field_def.field_type, value) {
998 ctx.add_error(
999 ErrorCode::TypeMismatch,
1000 format!(
1001 "Type mismatch: field '{}' expects {:?}, got {:?}",
1002 field,
1003 field_def.field_type,
1004 value_type_name(value)
1005 ),
1006 );
1007 }
1008 } else {
1009 ctx.add_error(
1010 ErrorCode::UnknownField,
1011 format!(
1012 "Filter field '{}' does not exist in collection '{}'",
1013 field, collection.name
1014 ),
1015 );
1016 }
1017}
1018
1019fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
1021 match (field_type, value) {
1022 (_, Value::Null) => true, (_, Value::Variable(_)) => true, (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
1025 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
1026 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
1027 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
1029 (FieldType::Array(_), Value::Array(_)) => true,
1030 (FieldType::Object, Value::Object(_)) => true,
1031 (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,
1037 }
1038}
1039
1040fn value_type_name(value: &Value) -> &'static str {
1042 match value {
1043 Value::Null => "null",
1044 Value::Boolean(_) => "boolean",
1045 Value::Int(_) => "int",
1046 Value::Float(_) => "float",
1047 Value::String(_) => "string",
1048 Value::Array(_) => "array",
1049 Value::Object(_) => "object",
1050 Value::Variable(_) => "variable",
1051 Value::Enum(_) => "enum",
1052 }
1053}
1054
1055fn report_unknown_filter_ops<S: SchemaProvider>(
1058 value: &Value,
1059 ctx: &mut ValidationContext<'_, S>,
1060) {
1061 const KNOWN_OPS: &[&str] = &[
1062 "eq", "ne", "gt", "gte", "lt", "lte",
1063 "in", "nin", "contains", "startsWith", "endsWith",
1064 "matches", "isNull", "isNotNull",
1065 ];
1066 if let Value::Object(map) = value {
1067 for (key, val) in map {
1068 match key.as_str() {
1069 "and" | "or" => {
1070 if let Value::Array(arr) = val {
1071 arr.iter().for_each(|v| report_unknown_filter_ops(v, ctx));
1072 }
1073 }
1074 "not" => report_unknown_filter_ops(val, ctx),
1075 field => {
1076 if let Value::Object(ops) = val {
1077 let has_known_op = ops.keys().any(|k| KNOWN_OPS.contains(&k.as_str()));
1081 if has_known_op {
1082 for op in ops.keys() {
1083 if !KNOWN_OPS.contains(&op.as_str()) {
1084 ctx.add_error(
1085 ErrorCode::InvalidArgument,
1086 format!(
1087 "Unknown filter operator '{}' on field '{}'",
1088 op, field
1089 ),
1090 );
1091 }
1092 }
1093 } else {
1094 report_unknown_filter_ops(val, ctx);
1096 }
1097 }
1098 }
1099 }
1100 }
1101 }
1102}
1103
1104fn extract_filter_from_value(value: &Value) -> Option<Filter> {
1106 match value {
1107 Value::Object(map) => {
1108 let mut filters = Vec::new();
1109
1110 for (key, val) in map {
1111 match key.as_str() {
1112 "and" => {
1113 if let Value::Array(arr) = val {
1114 let sub_filters: Vec<Filter> =
1115 arr.iter().filter_map(extract_filter_from_value).collect();
1116 if !sub_filters.is_empty() {
1117 filters.push(Filter::And(sub_filters));
1118 }
1119 }
1120 }
1121 "or" => {
1122 if let Value::Array(arr) = val {
1123 let sub_filters: Vec<Filter> =
1124 arr.iter().filter_map(extract_filter_from_value).collect();
1125 if !sub_filters.is_empty() {
1126 filters.push(Filter::Or(sub_filters));
1127 }
1128 }
1129 }
1130 "not" => {
1131 if let Some(inner) = extract_filter_from_value(val) {
1132 filters.push(Filter::Not(Box::new(inner)));
1133 }
1134 }
1135 field => {
1136 if let Value::Object(ops) = val {
1138 for (op, op_val) in ops {
1139 let filter = match op.as_str() {
1140 "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
1141 "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
1142 "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
1143 "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
1144 "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
1145 "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
1146 "in" => Some(Filter::In(field.to_string(), op_val.clone())),
1147 "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
1148 "contains" => {
1149 Some(Filter::Contains(field.to_string(), op_val.clone()))
1150 }
1151 "startsWith" => {
1152 Some(Filter::StartsWith(field.to_string(), op_val.clone()))
1153 }
1154 "endsWith" => {
1155 Some(Filter::EndsWith(field.to_string(), op_val.clone()))
1156 }
1157 "matches" => {
1158 Some(Filter::Matches(field.to_string(), op_val.clone()))
1159 }
1160 "isNull" => Some(Filter::IsNull(field.to_string())),
1161 "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
1162 _ => None,
1163 };
1164 if let Some(f) = filter {
1165 filters.push(f);
1166 }
1167 }
1168 }
1169 }
1170 }
1171 }
1172
1173 match filters.len() {
1174 0 => None,
1175 1 => Some(filters.remove(0)),
1176 _ => Some(Filter::And(filters)),
1177 }
1178 }
1179 _ => None,
1180 }
1181}
1182
1183pub fn resolve_variables(
1185 doc: &mut Document,
1186 variables: &HashMap<String, ast::Value>,
1187) -> Result<(), ValidationError> {
1188 for op in &mut doc.operations {
1189 match op {
1190 ast::Operation::Query(query) => {
1191 resolve_in_fields(&mut query.selection_set, variables)?;
1192 }
1193 ast::Operation::Mutation(mutation) => {
1194 for mut_op in &mut mutation.operations {
1195 resolve_in_mutation_op(mut_op, variables)?;
1196 }
1197 }
1198 ast::Operation::Subscription(sub) => {
1199 resolve_in_fields(&mut sub.selection_set, variables)?;
1200 }
1201 ast::Operation::Schema(_) => {}
1202 ast::Operation::Migration(_) => {}
1203 ast::Operation::FragmentDefinition(fragment) => {
1204 resolve_in_fields(&mut fragment.selection_set, variables)?;
1207 }
1208 ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
1211 }
1212 Ok(())
1213}
1214
1215fn resolve_in_fields(
1216 fields: &mut [Selection],
1217 variables: &HashMap<String, ast::Value>,
1218) -> Result<(), ValidationError> {
1219 for selection in fields {
1220 match selection {
1221 Selection::Field(field) => {
1222 for arg in &mut field.arguments {
1224 resolve_in_value(&mut arg.value, variables)?;
1225 }
1226 resolve_in_fields(&mut field.selection_set, variables)?;
1228 }
1229 Selection::InlineFragment(inline) => {
1230 resolve_in_fields(&mut inline.selection_set, variables)?;
1231 }
1232 Selection::FragmentSpread(_) => {
1233 }
1235 }
1236 }
1237 Ok(())
1238}
1239
1240fn resolve_in_mutation_op(
1241 op: &mut ast::MutationOperation,
1242 variables: &HashMap<String, ast::Value>,
1243) -> Result<(), ValidationError> {
1244 match &mut op.operation {
1245 ast::MutationOp::Insert { data, .. }
1246 | ast::MutationOp::Update { data, .. }
1247 | ast::MutationOp::Upsert { data, .. } => {
1248 resolve_in_value(data, variables)?;
1249 }
1250 ast::MutationOp::InsertMany { data, .. } => {
1251 for item in data {
1252 resolve_in_value(item, variables)?;
1253 }
1254 }
1255 ast::MutationOp::Delete { .. } => {}
1256 ast::MutationOp::EnqueueJob { payload, .. } => {
1257 resolve_in_value(payload, variables)?;
1258 }
1259 ast::MutationOp::EnqueueJobs { payloads, .. } => {
1260 for p in payloads {
1261 resolve_in_value(p, variables)?;
1262 }
1263 }
1264 ast::MutationOp::Import { data, .. } => {
1265 for item in data {
1266 resolve_in_value(item, variables)?;
1267 }
1268 }
1269 ast::MutationOp::Export { .. } => {}
1270 ast::MutationOp::Transaction { operations } => {
1271 for inner in operations {
1272 resolve_in_mutation_op(inner, variables)?;
1273 }
1274 }
1275 }
1276 resolve_in_fields(&mut op.selection_set, variables)
1277}
1278
1279fn resolve_in_value(
1280 value: &mut Value,
1281 variables: &HashMap<String, ast::Value>,
1282) -> Result<(), ValidationError> {
1283 match value {
1284 Value::Variable(name) => {
1285 if let Some(resolved) = variables.get(name) {
1286 *value = resolved.clone();
1287 } else {
1288 return Err(ValidationError::new(
1289 ErrorCode::MissingRequiredVariable,
1290 format!("Variable '{}' is not provided", name),
1291 ));
1292 }
1293 }
1294 Value::Array(items) => {
1295 for item in items {
1296 resolve_in_value(item, variables)?;
1297 }
1298 }
1299 Value::Object(map) => {
1300 for v in map.values_mut() {
1301 resolve_in_value(v, variables)?;
1302 }
1303 }
1304 _ => {}
1305 }
1306 Ok(())
1307}
1308
1309#[cfg(test)]
1310mod tests {
1311 use super::*;
1312 use crate::types::FieldDefinition;
1313
1314 fn create_test_schema() -> InMemorySchema {
1315 let mut schema = InMemorySchema::new();
1316
1317 let mut users_fields = HashMap::new();
1318 users_fields.insert(
1319 "name".to_string(),
1320 FieldDefinition {
1321 field_type: FieldType::SCALAR_STRING,
1322 unique: false,
1323 indexed: false,
1324 nullable: false,
1325 ..Default::default()
1326 },
1327 );
1328 users_fields.insert(
1329 "email".to_string(),
1330 FieldDefinition {
1331 field_type: FieldType::SCALAR_STRING,
1332 unique: true,
1333 indexed: true,
1334 nullable: false,
1335 ..Default::default()
1336 },
1337 );
1338 users_fields.insert(
1339 "age".to_string(),
1340 FieldDefinition {
1341 field_type: FieldType::SCALAR_INT,
1342 unique: false,
1343 indexed: false,
1344 nullable: false,
1345 ..Default::default()
1346 },
1347 );
1348 users_fields.insert(
1349 "active".to_string(),
1350 FieldDefinition {
1351 field_type: FieldType::SCALAR_BOOL,
1352 unique: false,
1353 indexed: false,
1354 nullable: false,
1355 ..Default::default()
1356 },
1357 );
1358
1359 schema.add_collection(Collection {
1360 name: "users".to_string(),
1361 fields: users_fields,
1362 });
1363
1364 schema
1365 }
1366
1367 #[test]
1368 fn test_validate_unknown_collection() {
1369 let schema = create_test_schema();
1370 let doc = Document {
1371 operations: vec![ast::Operation::Query(Query {
1372 name: None,
1373 variable_definitions: vec![],
1374 directives: vec![],
1375 selection_set: vec![Selection::Field(Field {
1376 alias: None,
1377 name: "nonexistent".to_string(),
1378 arguments: vec![],
1379 directives: vec![],
1380 selection_set: vec![Selection::Field(Field {
1381 alias: None,
1382 name: "id".to_string(),
1383 arguments: vec![],
1384 directives: vec![],
1385 selection_set: vec![],
1386 })],
1387 })],
1388 variables_values: HashMap::new(),
1389 })],
1390 };
1391
1392 let result = validate_document(&doc, &schema, HashMap::new());
1393 assert!(result.is_err());
1394 let errors = result.unwrap_err();
1395 assert!(
1396 errors
1397 .iter()
1398 .any(|e| e.code == ErrorCode::UnknownCollection)
1399 );
1400 }
1401
1402 #[test]
1403 fn test_validate_unknown_field() {
1404 let schema = create_test_schema();
1405 let doc = Document {
1406 operations: vec![ast::Operation::Query(Query {
1407 name: None,
1408 variable_definitions: vec![],
1409 directives: vec![],
1410 selection_set: vec![Selection::Field(Field {
1411 alias: None,
1412 name: "users".to_string(),
1413 arguments: vec![],
1414 directives: vec![],
1415 selection_set: vec![Selection::Field(Field {
1416 alias: None,
1417 name: "nonexistent_field".to_string(),
1418 arguments: vec![],
1419 directives: vec![],
1420 selection_set: vec![],
1421 })],
1422 })],
1423 variables_values: HashMap::new(),
1424 })],
1425 };
1426
1427 let result = validate_document(&doc, &schema, HashMap::new());
1428 assert!(result.is_err());
1429 let errors = result.unwrap_err();
1430 assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1431 }
1432
1433 #[test]
1434 fn test_validate_missing_required_variable() {
1435 let schema = create_test_schema();
1436 let doc = Document {
1437 operations: vec![ast::Operation::Query(Query {
1438 name: Some("GetUsers".to_string()),
1439 variable_definitions: vec![ast::VariableDefinition {
1440 name: "minAge".to_string(),
1441 var_type: ast::TypeAnnotation {
1442 name: "Int".to_string(),
1443 is_array: false,
1444 is_required: true,
1445 },
1446 default_value: None,
1447 }],
1448 directives: vec![],
1449 selection_set: vec![],
1450 variables_values: HashMap::new(),
1451 })],
1452 };
1453
1454 let result = validate_document(&doc, &schema, HashMap::new());
1456 assert!(result.is_err());
1457 let errors = result.unwrap_err();
1458 assert!(
1459 errors
1460 .iter()
1461 .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1462 );
1463 }
1464
1465 #[test]
1466 fn test_validate_valid_query() {
1467 let schema = create_test_schema();
1468 let doc = Document {
1469 operations: vec![ast::Operation::Query(Query {
1470 name: Some("GetUsers".to_string()),
1471 variable_definitions: vec![],
1472 directives: vec![],
1473 selection_set: vec![Selection::Field(Field {
1474 alias: None,
1475 name: "users".to_string(),
1476 arguments: vec![],
1477 directives: vec![],
1478 selection_set: vec![
1479 Selection::Field(Field {
1480 alias: None,
1481 name: "id".to_string(),
1482 arguments: vec![],
1483 directives: vec![],
1484 selection_set: vec![],
1485 }),
1486 Selection::Field(Field {
1487 alias: None,
1488 name: "name".to_string(),
1489 arguments: vec![],
1490 directives: vec![],
1491 selection_set: vec![],
1492 }),
1493 Selection::Field(Field {
1494 alias: None,
1495 name: "email".to_string(),
1496 arguments: vec![],
1497 directives: vec![],
1498 selection_set: vec![],
1499 }),
1500 ],
1501 })],
1502 variables_values: HashMap::new(),
1503 })],
1504 };
1505
1506 let result = validate_document(&doc, &schema, HashMap::new());
1507 assert!(result.is_ok());
1508 }
1509
1510 #[test]
1511 fn test_validate_filter_type_mismatch() {
1512 let schema = create_test_schema();
1513 let collection = schema.get_collection("users").unwrap();
1514 let mut ctx = ValidationContext::new(&schema);
1515
1516 let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1518 validate_filter(&filter, collection, &mut ctx);
1519
1520 assert!(ctx.has_errors());
1521 assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1522 }
1523
1524 #[test]
1525 fn test_validate_filter_string_operator_on_int() {
1526 let schema = create_test_schema();
1527 let collection = schema.get_collection("users").unwrap();
1528 let mut ctx = ValidationContext::new(&schema);
1529
1530 let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1532 validate_filter(&filter, collection, &mut ctx);
1533
1534 assert!(ctx.has_errors());
1535 assert!(
1536 ctx.errors
1537 .iter()
1538 .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1539 );
1540 }
1541
1542 #[test]
1543 fn test_resolve_variables() {
1544 let mut doc = Document {
1545 operations: vec![ast::Operation::Query(Query {
1546 name: None,
1547 variable_definitions: vec![],
1548 directives: vec![],
1549 selection_set: vec![Selection::Field(Field {
1550 alias: None,
1551 name: "users".to_string(),
1552 arguments: vec![ast::Argument {
1553 name: "limit".to_string(),
1554 value: Value::Variable("pageSize".to_string()),
1555 }],
1556 directives: vec![],
1557 selection_set: vec![],
1558 })],
1559 variables_values: HashMap::new(),
1560 })],
1561 };
1562
1563 let mut vars = HashMap::new();
1564 vars.insert("pageSize".to_string(), Value::Int(10));
1565
1566 let result = resolve_variables(&mut doc, &vars);
1567 assert!(result.is_ok());
1568
1569 if let ast::Operation::Query(query) = &doc.operations[0] {
1571 if let Selection::Field(user_field) = &query.selection_set[0] {
1572 let arg = &user_field.arguments[0];
1573 assert!(matches!(arg.value, Value::Int(10)));
1574 } else {
1575 panic!("Expected Selection::Field");
1576 }
1577 } else {
1578 panic!("Expected Query operation");
1579 }
1580 }
1581}