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 Selection::ComputedField(_) => {}
263 }
264 }
265
266 if query.name.is_some() {
267 ctx.pop_path();
268 }
269}
270
271fn validate_mutation<S: SchemaProvider>(mutation: &Mutation, ctx: &mut ValidationContext<'_, S>) {
273 if let Some(name) = &mutation.name {
274 ctx.push_path(name);
275 }
276
277 validate_variable_definitions(&mutation.variable_definitions, ctx);
279
280 for (i, op) in mutation.operations.iter().enumerate() {
282 ctx.push_path(&format!("mutation[{}]", i));
283 validate_mutation_operation(op, ctx);
284 ctx.pop_path();
285 }
286
287 if mutation.name.is_some() {
288 ctx.pop_path();
289 }
290}
291
292fn validate_inline_fragment<S: SchemaProvider>(
298 inline: &ast::InlineFragment,
299 expected_collection: Option<&str>,
300 ctx: &mut ValidationContext<'_, S>,
301) {
302 if let Some(enc) = expected_collection {
304 if inline.type_condition != enc {
305 ctx.add_error(
306 ErrorCode::TypeMismatch,
307 format!(
308 "Inline fragment on '{}' cannot appear inside '{}'",
309 inline.type_condition, enc
310 ),
311 );
312 return;
313 }
314 }
315
316 if !ctx.schema.collection_exists(&inline.type_condition) {
318 ctx.add_error(
319 ErrorCode::UnknownCollection,
320 format!(
321 "Unknown collection '{}' in inline fragment",
322 inline.type_condition
323 ),
324 );
325 return;
326 }
327
328 if let Some(collection) = ctx.schema.get_collection(&inline.type_condition) {
330 validate_selection_set(&inline.selection_set, collection, ctx);
331 }
332}
333
334fn validate_subscription<S: SchemaProvider>(
336 sub: &Subscription,
337 ctx: &mut ValidationContext<'_, S>,
338) {
339 if let Some(name) = &sub.name {
340 ctx.push_path(name);
341 }
342
343 validate_variable_definitions(&sub.variable_definitions, ctx);
345
346 for selection in &sub.selection_set {
348 match selection {
349 Selection::Field(field) => {
350 validate_field(field, None, ctx);
351 }
352 Selection::InlineFragment(inline) => {
353 validate_inline_fragment(inline, None, ctx);
354 }
355 Selection::FragmentSpread(fragment_name) => {
356 if let Some(fragment) = ctx.fragments.get(fragment_name) {
357 validate_fragment_spread(fragment_name, &fragment.type_condition, ctx);
358 } else {
359 ctx.add_error(
360 ErrorCode::UnknownFragment,
361 format!("Fragment '{}' is not defined", fragment_name),
362 );
363 }
364 }
365 Selection::ComputedField(_) => {}
366 }
367 }
368
369 if sub.name.is_some() {
370 ctx.pop_path();
371 }
372}
373
374fn validate_variable_definitions<S: SchemaProvider>(
376 definitions: &[ast::VariableDefinition],
377 ctx: &mut ValidationContext<'_, S>,
378) {
379 for def in definitions {
380 let var_name = &def.name;
381
382 if def.var_type.is_required && def.default_value.is_none() {
384 if !ctx.variables.contains_key(var_name) {
385 ctx.add_error(
386 ErrorCode::MissingRequiredVariable,
387 format!("Required variable '{}' is not provided", var_name),
388 );
389 }
390 }
391 }
392}
393
394fn validate_field<S: SchemaProvider>(
399 field: &Field,
400 parent_collection: Option<&Collection>,
401 ctx: &mut ValidationContext<'_, S>,
402) {
403 let field_name = field.alias.as_ref().unwrap_or(&field.name);
404 ctx.push_path(field_name);
405
406 match parent_collection {
407 None => {
408 let collection_name = &field.name;
410 if !ctx.schema.collection_exists(collection_name) {
411 ctx.add_error(
412 ErrorCode::UnknownCollection,
413 format!("Collection '{}' does not exist", collection_name),
414 );
415 } else if field.selection_set.is_empty() {
416 ctx.add_error(
417 ErrorCode::InvalidInput,
418 format!("Collection '{}' requires a selection set", collection_name),
419 );
420 } else if let Some(collection) = ctx.schema.get_collection(collection_name) {
421 validate_selection_set(&field.selection_set, collection, ctx);
423
424 for arg in &field.arguments {
426 if arg.name == "where" || arg.name == "filter" {
427 report_unknown_filter_ops(&arg.value, ctx);
428 if let Some(filter) = extract_filter_from_value(&arg.value) {
429 validate_filter(&filter, collection, ctx);
430 }
431 }
432 }
433 }
434 }
435 Some(collection) => {
436 if !field.selection_set.is_empty() {
438 if let Some(field_def) = collection.fields.get(&field.name) {
439 match &field_def.field_type {
440 FieldType::Nested(nested_schema) => {
441 validate_nested_selection_set(&field.selection_set, nested_schema, ctx);
443 }
444 FieldType::Object | FieldType::Any => {
445 }
448 FieldType::Scalar(ScalarType::Object)
449 | FieldType::Scalar(ScalarType::Any) => {
450 }
452 _ => {
453 ctx.add_error(
455 ErrorCode::TypeMismatch,
456 format!(
457 "Field '{}' is not an object type but has a selection set",
458 field.name
459 ),
460 );
461 }
462 }
463 }
464 }
466 }
467 }
468
469 ctx.pop_path();
470}
471
472fn validate_selection_set<S: SchemaProvider>(
474 selections: &[Selection],
475 collection: &Collection,
476 ctx: &mut ValidationContext<'_, S>,
477) {
478 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
479
480 for selection in selections {
481 match selection {
483 Selection::Field(field) => {
484 let display_name = field.alias.as_ref().unwrap_or(&field.name);
485
486 if aliases_seen.contains_key(display_name) {
488 ctx.add_error(
489 ErrorCode::DuplicateAlias,
490 format!("Duplicate field/alias '{}' in selection", display_name),
491 );
492 }
493 aliases_seen.insert(display_name.clone(), true);
494
495 let field_name = &field.name;
497 if !field_name.starts_with("__") && field_name != "id" {
498 if !collection.fields.contains_key(field_name) {
499 ctx.add_error(
500 ErrorCode::UnknownField,
501 format!(
502 "Field '{}' does not exist in collection '{}'",
503 field_name, collection.name
504 ),
505 );
506 }
507 }
508
509 validate_field(field, Some(collection), ctx);
511 }
512 Selection::InlineFragment(inline) => {
513 validate_inline_fragment(inline, Some(&collection.name), ctx);
514 }
515 Selection::FragmentSpread(fragment_name) => {
516 validate_fragment_spread(fragment_name, &collection.name, ctx);
517 }
518 Selection::ComputedField(cf) => {
519 let display_name = &cf.alias;
520 if aliases_seen.contains_key(display_name) {
521 ctx.add_error(
522 ErrorCode::DuplicateAlias,
523 format!("Duplicate field/alias '{}' in selection", display_name),
524 );
525 }
526 aliases_seen.insert(display_name.clone(), true);
527 }
528 }
529 }
530}
531
532fn validate_nested_selection_set<S: SchemaProvider>(
534 selections: &[Selection],
535 nested_schema: &HashMap<String, crate::types::FieldDefinition>,
536 ctx: &mut ValidationContext<'_, S>,
537) {
538 let mut aliases_seen: HashMap<String, bool> = HashMap::new();
539
540 for selection in selections {
541 match selection {
542 Selection::Field(field) => {
543 let display_name = field.alias.as_ref().unwrap_or(&field.name);
544
545 if aliases_seen.contains_key(display_name) {
547 ctx.add_error(
548 ErrorCode::DuplicateAlias,
549 format!("Duplicate field/alias '{}' in selection", display_name),
550 );
551 }
552 aliases_seen.insert(display_name.clone(), true);
553
554 let field_name = &field.name;
556 if !field_name.starts_with("__") {
557 if !nested_schema.contains_key(field_name) {
558 ctx.add_error(
559 ErrorCode::UnknownField,
560 format!("Field '{}' does not exist in nested object", field_name),
561 );
562 } else if !field.selection_set.is_empty() {
563 if let Some(field_def) = nested_schema.get(field_name) {
565 match &field_def.field_type {
566 FieldType::Nested(deeper_schema) => {
567 ctx.push_path(field_name);
568 validate_nested_selection_set(
569 &field.selection_set,
570 deeper_schema,
571 ctx,
572 );
573 ctx.pop_path();
574 }
575 FieldType::Object | FieldType::Any => {
576 }
578 FieldType::Scalar(ScalarType::Object)
579 | FieldType::Scalar(ScalarType::Any) => {
580 }
582 _ => {
583 ctx.add_error(
584 ErrorCode::TypeMismatch,
585 format!(
586 "Field '{}' is not an object type but has a selection set",
587 field_name
588 ),
589 );
590 }
591 }
592 }
593 }
594 }
595 }
596 Selection::InlineFragment(_) | Selection::FragmentSpread(_) => {
597 ctx.add_error(
599 ErrorCode::InvalidInput,
600 "Fragments are not supported in nested object selections".to_string(),
601 );
602 }
603 Selection::ComputedField(cf) => {
604 let display_name = &cf.alias;
605 if aliases_seen.contains_key(display_name) {
606 ctx.add_error(
607 ErrorCode::DuplicateAlias,
608 format!("Duplicate field/alias '{}' in selection", display_name),
609 );
610 }
611 aliases_seen.insert(display_name.clone(), true);
612 }
613 }
614 }
615}
616
617fn validate_fragment_spread<S: SchemaProvider>(
619 fragment_name: &str,
620 expected_collection: &str,
621 ctx: &mut ValidationContext<'_, S>,
622) {
623 if ctx.validating_fragments.contains(fragment_name) {
625 ctx.add_error(
626 ErrorCode::InvalidInput,
627 format!("Fragment '{}' contains a cyclic reference", fragment_name),
628 );
629 return;
630 }
631
632 let fragment_opt = ctx.fragments.get(fragment_name).cloned();
634
635 if let Some(fragment) = fragment_opt {
636 if fragment.type_condition != expected_collection {
638 ctx.add_error(
639 ErrorCode::TypeMismatch,
640 format!(
641 "Fragment '{}' is defined on '{}' but used on '{}'",
642 fragment_name, fragment.type_condition, expected_collection
643 ),
644 );
645 return;
646 }
647
648 if let Some(collection) = ctx.schema.get_collection(expected_collection) {
650 ctx.push_path(&format!("...{}", fragment_name));
651 ctx.validating_fragments.insert(fragment_name.to_string());
652 validate_selection_set(&fragment.selection_set, collection, ctx);
653 ctx.validating_fragments.remove(fragment_name);
654 ctx.pop_path();
655 } else {
656 ctx.add_error(
657 ErrorCode::UnknownCollection,
658 format!(
659 "Fragment '{}' references unknown collection '{}'",
660 fragment_name, expected_collection
661 ),
662 );
663 }
664 } else {
665 ctx.add_error(
666 ErrorCode::UnknownFragment,
667 format!("Fragment '{}' is not defined", fragment_name),
668 );
669 }
670}
671
672fn validate_mutation_operation<S: SchemaProvider>(
674 op: &ast::MutationOperation,
675 ctx: &mut ValidationContext<'_, S>,
676) {
677 match &op.operation {
678 ast::MutationOp::Insert { collection, data } => {
679 if let Some(col_def) = ctx.schema.get_collection(collection) {
680 validate_object_against_schema(data, col_def, ctx);
681 } else {
682 ctx.add_error(
683 ErrorCode::UnknownCollection,
684 format!("Collection '{}' does not exist", collection),
685 );
686 }
687 }
688 ast::MutationOp::InsertMany { collection, data } => {
689 if let Some(col_def) = ctx.schema.get_collection(collection) {
690 if let ast::Value::Array(items) = data {
691 for item in items {
692 validate_object_against_schema(item, col_def, ctx);
693 }
694 }
695 } else {
696 ctx.add_error(
697 ErrorCode::UnknownCollection,
698 format!("Collection '{}' does not exist", collection),
699 );
700 }
701 }
702 ast::MutationOp::Update {
703 collection, data, ..
704 }
705 | ast::MutationOp::Upsert {
706 collection, data, ..
707 } => {
708 if let Some(col_def) = ctx.schema.get_collection(collection) {
709 validate_partial_object(data, col_def, ctx);
711 } else {
712 ctx.add_error(
713 ErrorCode::UnknownCollection,
714 format!("Collection '{}' does not exist", collection),
715 );
716 }
717 }
718 ast::MutationOp::Delete { collection, .. } => {
719 if !ctx.schema.collection_exists(collection) {
720 ctx.add_error(
721 ErrorCode::UnknownCollection,
722 format!("Collection '{}' does not exist", collection),
723 );
724 }
725 }
726 ast::MutationOp::EnqueueJob { .. } => {}
727 ast::MutationOp::EnqueueJobs { .. } => {}
728 ast::MutationOp::Import { collection, data } => {
729 if let Some(col_def) = ctx.schema.get_collection(collection) {
730 for item in data {
731 validate_object_against_schema(item, col_def, ctx);
732 }
733 }
734 }
735 ast::MutationOp::Export { .. } => {}
736 ast::MutationOp::Transaction { operations } => {
737 for (i, inner_op) in operations.iter().enumerate() {
738 ctx.push_path(&format!("tx[{}]", i));
739 validate_mutation_operation(inner_op, ctx);
740 ctx.pop_path();
741 }
742 }
743 }
744}
745
746fn validate_object_against_schema<S: SchemaProvider>(
748 value: &Value,
749 collection: &Collection,
750 ctx: &mut ValidationContext<'_, S>,
751) {
752 match value {
753 Value::Object(map) => {
754 for (key, val) in map {
756 if let Some(field_def) = collection.fields.get(key) {
757 if matches!(val, Value::Null) && !field_def.nullable {
759 ctx.add_error(
760 ErrorCode::InvalidInput,
761 format!("Field '{}' cannot be null", key),
762 );
763 } else if !matches!(val, Value::Null)
764 && !validate_value_against_type(val, &field_def.field_type)
765 {
766 ctx.add_error(
767 ErrorCode::TypeMismatch,
768 format!(
769 "Type mismatch for field '{}': expected {:?}, got {:?}",
770 key,
771 field_def.field_type,
772 value_type_name(val)
773 ),
774 );
775 }
776 } else if key != "id" {
777 ctx.add_error(
778 ErrorCode::UnknownField,
779 format!(
780 "Field '{}' not defined in collection '{}'",
781 key, collection.name
782 ),
783 );
784 }
785 }
786
787 for (name, def) in &collection.fields {
789 if !def.nullable && !map.contains_key(name) {
790 ctx.add_error(
791 ErrorCode::InvalidInput,
792 format!("Missing required field '{}'", name),
793 );
794 }
795 }
796 }
797 _ => {
798 ctx.add_error(
799 ErrorCode::TypeMismatch,
800 format!(
801 "Expected object for collection '{}', got {:?}",
802 collection.name,
803 value_type_name(value)
804 ),
805 );
806 }
807 }
808}
809
810fn validate_partial_object<S: SchemaProvider>(
813 value: &Value,
814 collection: &Collection,
815 ctx: &mut ValidationContext<'_, S>,
816) {
817 match value {
818 Value::Object(map) => {
819 for (key, val) in map {
821 if let Some(field_def) = collection.fields.get(key) {
822 if matches!(val, Value::Null) && !field_def.nullable {
824 ctx.add_error(
825 ErrorCode::InvalidInput,
826 format!("Field '{}' cannot be null", key),
827 );
828 } else if !matches!(val, Value::Null)
829 && !validate_value_against_type(val, &field_def.field_type)
830 {
831 ctx.add_error(
832 ErrorCode::TypeMismatch,
833 format!(
834 "Type mismatch for field '{}': expected {:?}, got {:?}",
835 key,
836 field_def.field_type,
837 value_type_name(val)
838 ),
839 );
840 }
841 } else if key != "id" {
842 ctx.add_error(
843 ErrorCode::UnknownField,
844 format!(
845 "Field '{}' not defined in collection '{}'",
846 key, collection.name
847 ),
848 );
849 }
850 }
851 }
852 _ => {
853 ctx.add_error(
854 ErrorCode::TypeMismatch,
855 format!(
856 "Expected object for collection '{}', got {:?}",
857 collection.name,
858 value_type_name(value)
859 ),
860 );
861 }
862 }
863}
864
865fn validate_value_against_type(value: &Value, expected: &FieldType) -> bool {
867 use crate::types::ScalarType;
868 match (expected, value) {
869 (FieldType::Scalar(ScalarType::Any), _) => true,
871 (_, Value::Null) => true,
872 (_, Value::Variable(_)) => true,
873
874 (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
875 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
876 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
877 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true,
878 (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
879 (FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(),
880
881 (FieldType::Array(scalar_inner), Value::Array(items)) => {
883 let inner_field_type = FieldType::Scalar(scalar_inner.clone());
885 items
886 .iter()
887 .all(|item| validate_value_against_type(item, &inner_field_type))
888 }
889
890 (FieldType::Object, Value::Object(_)) => true,
892 (FieldType::Nested(schema), Value::Object(map)) => {
894 for (key, val) in map {
896 if let Some(def) = schema.get(key) {
897 if matches!(val, Value::Null) {
898 if !def.nullable {
899 return false; }
901 } else if !validate_value_against_type(val, &def.field_type) {
902 return false; }
904 } else {
905 return false; }
907 }
908 for (key, def) in schema.iter() {
910 if !def.nullable && !map.contains_key(key) {
911 return false; }
913 }
914 true
915 }
916 (FieldType::Nested(_), _) => false, (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true,
920 (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true,
922
923 _ => false,
924 }
925}
926
927fn validate_filter<S: SchemaProvider>(
929 filter: &Filter,
930 collection: &Collection,
931 ctx: &mut ValidationContext<'_, S>,
932) {
933 match filter {
934 Filter::Eq(field, value)
935 | Filter::Ne(field, value)
936 | Filter::Gt(field, value)
937 | Filter::Gte(field, value)
938 | Filter::Lt(field, value)
939 | Filter::Lte(field, value) => {
940 validate_filter_field(field, value, collection, ctx);
941 }
942 Filter::In(field, value) | Filter::NotIn(field, value) => {
943 if !matches!(value, Value::Array(_)) {
945 ctx.add_error(
946 ErrorCode::TypeMismatch,
947 format!("Filter 'in'/'notIn' on '{}' requires an array value", field),
948 );
949 }
950 validate_filter_field_exists(field, collection, ctx);
951 }
952 Filter::ContainsAny(field, value) | Filter::ContainsAll(field, value) => {
953 if !matches!(value, Value::Array(_) | Value::Variable(_)) {
954 ctx.add_error(
955 ErrorCode::TypeMismatch,
956 format!(
957 "containsAny/containsAll on field '{}' expects an array",
958 field
959 ),
960 );
961 }
962 validate_filter_field_exists(field, collection, ctx);
963 }
964 Filter::Contains(field, _)
965 | Filter::StartsWith(field, _)
966 | Filter::EndsWith(field, _)
967 | Filter::Matches(field, _) => {
968 if let Some(field_def) = collection.fields.get(field) {
970 if field_def.field_type != FieldType::SCALAR_STRING {
971 ctx.add_error(
972 ErrorCode::InvalidFilterOperator,
973 format!("String operator on non-string field '{}'", field),
974 );
975 }
976 } else {
977 validate_filter_field_exists(field, collection, ctx);
978 }
979 }
980 Filter::IsNull(field) | Filter::IsNotNull(field) => {
981 validate_filter_field_exists(field, collection, ctx);
982 }
983 Filter::And(filters) | Filter::Or(filters) => {
984 for f in filters {
985 validate_filter(f, collection, ctx);
986 }
987 }
988 Filter::Not(inner) => {
989 validate_filter(inner, collection, ctx);
990 }
991 }
992}
993
994fn validate_filter_field_exists<S: SchemaProvider>(
996 field: &str,
997 collection: &Collection,
998 ctx: &mut ValidationContext<'_, S>,
999) {
1000 if field != "id" && !collection.fields.contains_key(field) {
1001 ctx.add_error(
1002 ErrorCode::UnknownField,
1003 format!(
1004 "Filter field '{}' does not exist in collection '{}'",
1005 field, collection.name
1006 ),
1007 );
1008 }
1009}
1010
1011fn validate_filter_field<S: SchemaProvider>(
1013 field: &str,
1014 value: &Value,
1015 collection: &Collection,
1016 ctx: &mut ValidationContext<'_, S>,
1017) {
1018 if field == "id" {
1019 return; }
1021
1022 if let Some(field_def) = collection.fields.get(field) {
1023 if !is_type_compatible(&field_def.field_type, value) {
1025 ctx.add_error(
1026 ErrorCode::TypeMismatch,
1027 format!(
1028 "Type mismatch: field '{}' expects {:?}, got {:?}",
1029 field,
1030 field_def.field_type,
1031 value_type_name(value)
1032 ),
1033 );
1034 }
1035 } else {
1036 ctx.add_error(
1037 ErrorCode::UnknownField,
1038 format!(
1039 "Filter field '{}' does not exist in collection '{}'",
1040 field, collection.name
1041 ),
1042 );
1043 }
1044}
1045
1046fn is_type_compatible(field_type: &FieldType, value: &Value) -> bool {
1048 match (field_type, value) {
1049 (_, Value::Null) => true, (_, Value::Variable(_)) => true, (FieldType::Scalar(ScalarType::String), Value::String(_)) => true,
1052 (FieldType::Scalar(ScalarType::Int), Value::Int(_)) => true,
1053 (FieldType::Scalar(ScalarType::Float), Value::Float(_)) => true,
1054 (FieldType::Scalar(ScalarType::Float), Value::Int(_)) => true, (FieldType::Scalar(ScalarType::Bool), Value::Boolean(_)) => true,
1056 (FieldType::Array(_), Value::Array(_)) => true,
1057 (FieldType::Object, Value::Object(_)) => true,
1058 (FieldType::Nested(_), Value::Object(_)) => true, (FieldType::Scalar(ScalarType::Object), Value::Object(_)) => true, (FieldType::Scalar(ScalarType::Array), Value::Array(_)) => true, (FieldType::Scalar(ScalarType::Any), _) => true, (FieldType::Scalar(ScalarType::Uuid), Value::String(s)) => uuid::Uuid::parse_str(s).is_ok(), _ => false,
1064 }
1065}
1066
1067fn value_type_name(value: &Value) -> &'static str {
1069 match value {
1070 Value::Null => "null",
1071 Value::Boolean(_) => "boolean",
1072 Value::Int(_) => "int",
1073 Value::Float(_) => "float",
1074 Value::String(_) => "string",
1075 Value::Array(_) => "array",
1076 Value::Object(_) => "object",
1077 Value::Variable(_) => "variable",
1078 Value::Enum(_) => "enum",
1079 }
1080}
1081
1082fn report_unknown_filter_ops<S: SchemaProvider>(value: &Value, ctx: &mut ValidationContext<'_, S>) {
1085 const KNOWN_OPS: &[&str] = &[
1086 "eq",
1087 "ne",
1088 "gt",
1089 "gte",
1090 "lt",
1091 "lte",
1092 "in",
1093 "nin",
1094 "contains",
1095 "startsWith",
1096 "endsWith",
1097 "matches",
1098 "isNull",
1099 "isNotNull",
1100 ];
1101 if let Value::Object(map) = value {
1102 for (key, val) in map {
1103 match key.as_str() {
1104 "and" | "or" => {
1105 if let Value::Array(arr) = val {
1106 arr.iter().for_each(|v| report_unknown_filter_ops(v, ctx));
1107 }
1108 }
1109 "not" => report_unknown_filter_ops(val, ctx),
1110 field => {
1111 if let Value::Object(ops) = val {
1112 let has_known_op = ops.keys().any(|k| KNOWN_OPS.contains(&k.as_str()));
1116 if has_known_op {
1117 for op in ops.keys() {
1118 if !KNOWN_OPS.contains(&op.as_str()) {
1119 ctx.add_error(
1120 ErrorCode::InvalidArgument,
1121 format!(
1122 "Unknown filter operator '{}' on field '{}'",
1123 op, field
1124 ),
1125 );
1126 }
1127 }
1128 } else {
1129 report_unknown_filter_ops(val, ctx);
1131 }
1132 }
1133 }
1134 }
1135 }
1136 }
1137}
1138
1139fn extract_filter_from_value(value: &Value) -> Option<Filter> {
1141 match value {
1142 Value::Object(map) => {
1143 let mut filters = Vec::new();
1144
1145 for (key, val) in map {
1146 match key.as_str() {
1147 "and" => {
1148 if let Value::Array(arr) = val {
1149 let sub_filters: Vec<Filter> =
1150 arr.iter().filter_map(extract_filter_from_value).collect();
1151 if !sub_filters.is_empty() {
1152 filters.push(Filter::And(sub_filters));
1153 }
1154 }
1155 }
1156 "or" => {
1157 if let Value::Array(arr) = val {
1158 let sub_filters: Vec<Filter> =
1159 arr.iter().filter_map(extract_filter_from_value).collect();
1160 if !sub_filters.is_empty() {
1161 filters.push(Filter::Or(sub_filters));
1162 }
1163 }
1164 }
1165 "not" => {
1166 if let Some(inner) = extract_filter_from_value(val) {
1167 filters.push(Filter::Not(Box::new(inner)));
1168 }
1169 }
1170 field => {
1171 if let Value::Object(ops) = val {
1173 for (op, op_val) in ops {
1174 let filter = match op.as_str() {
1175 "eq" => Some(Filter::Eq(field.to_string(), op_val.clone())),
1176 "ne" => Some(Filter::Ne(field.to_string(), op_val.clone())),
1177 "gt" => Some(Filter::Gt(field.to_string(), op_val.clone())),
1178 "gte" => Some(Filter::Gte(field.to_string(), op_val.clone())),
1179 "lt" => Some(Filter::Lt(field.to_string(), op_val.clone())),
1180 "lte" => Some(Filter::Lte(field.to_string(), op_val.clone())),
1181 "in" => Some(Filter::In(field.to_string(), op_val.clone())),
1182 "nin" => Some(Filter::NotIn(field.to_string(), op_val.clone())),
1183 "contains" => {
1184 Some(Filter::Contains(field.to_string(), op_val.clone()))
1185 }
1186 "startsWith" => {
1187 Some(Filter::StartsWith(field.to_string(), op_val.clone()))
1188 }
1189 "endsWith" => {
1190 Some(Filter::EndsWith(field.to_string(), op_val.clone()))
1191 }
1192 "matches" => {
1193 Some(Filter::Matches(field.to_string(), op_val.clone()))
1194 }
1195 "isNull" => Some(Filter::IsNull(field.to_string())),
1196 "isNotNull" => Some(Filter::IsNotNull(field.to_string())),
1197 _ => None,
1198 };
1199 if let Some(f) = filter {
1200 filters.push(f);
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207
1208 match filters.len() {
1209 0 => None,
1210 1 => Some(filters.remove(0)),
1211 _ => Some(Filter::And(filters)),
1212 }
1213 }
1214 _ => None,
1215 }
1216}
1217
1218pub fn resolve_variables(
1220 doc: &mut Document,
1221 variables: &HashMap<String, ast::Value>,
1222) -> Result<(), ValidationError> {
1223 for op in &mut doc.operations {
1224 match op {
1225 ast::Operation::Query(query) => {
1226 resolve_in_fields(&mut query.selection_set, variables)?;
1227 }
1228 ast::Operation::Mutation(mutation) => {
1229 for mut_op in &mut mutation.operations {
1230 resolve_in_mutation_op(mut_op, variables)?;
1231 }
1232 }
1233 ast::Operation::Subscription(sub) => {
1234 resolve_in_fields(&mut sub.selection_set, variables)?;
1235 }
1236 ast::Operation::Schema(_) => {}
1237 ast::Operation::Migration(_) => {}
1238 ast::Operation::FragmentDefinition(fragment) => {
1239 resolve_in_fields(&mut fragment.selection_set, variables)?;
1242 }
1243 ast::Operation::Introspection(_) => {} ast::Operation::Handler(_) => {} }
1246 }
1247 Ok(())
1248}
1249
1250fn resolve_in_fields(
1251 fields: &mut [Selection],
1252 variables: &HashMap<String, ast::Value>,
1253) -> Result<(), ValidationError> {
1254 for selection in fields {
1255 match selection {
1256 Selection::Field(field) => {
1257 for arg in &mut field.arguments {
1259 resolve_in_value(&mut arg.value, variables)?;
1260 }
1261 resolve_in_fields(&mut field.selection_set, variables)?;
1263 }
1264 Selection::InlineFragment(inline) => {
1265 resolve_in_fields(&mut inline.selection_set, variables)?;
1266 }
1267 Selection::FragmentSpread(_) => {
1268 }
1270 Selection::ComputedField(cf) => {
1271 resolve_in_computed_expression(&mut cf.expression, variables)?;
1272 }
1273 }
1274 }
1275 Ok(())
1276}
1277
1278fn resolve_in_computed_expression(
1279 expr: &mut ast::ComputedExpression,
1280 variables: &HashMap<String, ast::Value>,
1281) -> Result<(), ValidationError> {
1282 match expr {
1283 ast::ComputedExpression::FunctionCall { args, .. } => {
1284 for arg in args {
1285 resolve_in_expression(arg, variables)?;
1286 }
1287 }
1288 ast::ComputedExpression::PipeExpression { base, operations } => {
1289 resolve_in_expression(base, variables)?;
1290 for op in operations {
1291 for arg in &mut op.args {
1292 resolve_in_expression(arg, variables)?;
1293 }
1294 }
1295 }
1296 _ => {}
1297 }
1298 Ok(())
1299}
1300
1301fn resolve_in_expression(
1302 expr: &mut ast::Expression,
1303 variables: &HashMap<String, ast::Value>,
1304) -> Result<(), ValidationError> {
1305 match expr {
1306 ast::Expression::Literal(v) => resolve_in_value(v, variables)?,
1307 ast::Expression::Variable(name) => {
1308 if let Some(val) = variables.get(name) {
1309 *expr = ast::Expression::Literal(val.clone());
1310 }
1311 }
1312 ast::Expression::FunctionCall { args, .. } => {
1313 for arg in args {
1314 resolve_in_expression(arg, variables)?;
1315 }
1316 }
1317 ast::Expression::Binary { left, right, .. } => {
1318 resolve_in_expression(left, variables)?;
1319 resolve_in_expression(right, variables)?;
1320 }
1321 ast::Expression::Unary { expr, .. } => {
1322 resolve_in_expression(expr, variables)?;
1323 }
1324 ast::Expression::Ternary {
1325 condition,
1326 then_expr,
1327 else_expr,
1328 } => {
1329 resolve_in_expression(condition, variables)?;
1330 resolve_in_expression(then_expr, variables)?;
1331 resolve_in_expression(else_expr, variables)?;
1332 }
1333 _ => {}
1334 }
1335 Ok(())
1336}
1337
1338fn resolve_in_mutation_op(
1339 op: &mut ast::MutationOperation,
1340 variables: &HashMap<String, ast::Value>,
1341) -> Result<(), ValidationError> {
1342 match &mut op.operation {
1343 ast::MutationOp::Insert { data, .. }
1344 | ast::MutationOp::Update { data, .. }
1345 | ast::MutationOp::Upsert { data, .. } => {
1346 resolve_in_value(data, variables)?;
1347 }
1348 ast::MutationOp::InsertMany { data, .. } => {
1349 resolve_in_value(data, variables)?;
1350 }
1351 ast::MutationOp::Delete { .. } => {}
1352 ast::MutationOp::EnqueueJob { payload, .. } => {
1353 resolve_in_value(payload, variables)?;
1354 }
1355 ast::MutationOp::EnqueueJobs { payloads, .. } => {
1356 for p in payloads {
1357 resolve_in_value(p, variables)?;
1358 }
1359 }
1360 ast::MutationOp::Import { data, .. } => {
1361 for item in data {
1362 resolve_in_value(item, variables)?;
1363 }
1364 }
1365 ast::MutationOp::Export { .. } => {}
1366 ast::MutationOp::Transaction { operations } => {
1367 for inner in operations {
1368 resolve_in_mutation_op(inner, variables)?;
1369 }
1370 }
1371 }
1372 resolve_in_fields(&mut op.selection_set, variables)
1373}
1374
1375fn resolve_in_value(
1376 value: &mut Value,
1377 variables: &HashMap<String, ast::Value>,
1378) -> Result<(), ValidationError> {
1379 match value {
1380 Value::Variable(name) => {
1381 if let Some(resolved) = variables.get(name) {
1382 *value = resolved.clone();
1383 } else {
1384 return Err(ValidationError::new(
1385 ErrorCode::MissingRequiredVariable,
1386 format!("Variable '{}' is not provided", name),
1387 ));
1388 }
1389 }
1390 Value::Array(items) => {
1391 for item in items {
1392 resolve_in_value(item, variables)?;
1393 }
1394 }
1395 Value::Object(map) => {
1396 for v in map.values_mut() {
1397 resolve_in_value(v, variables)?;
1398 }
1399 }
1400 _ => {}
1401 }
1402 Ok(())
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407 use super::*;
1408 use crate::types::FieldDefinition;
1409
1410 fn create_test_schema() -> InMemorySchema {
1411 let mut schema = InMemorySchema::new();
1412
1413 let mut users_fields = HashMap::new();
1414 users_fields.insert(
1415 "name".to_string(),
1416 FieldDefinition {
1417 field_type: FieldType::SCALAR_STRING,
1418 unique: false,
1419 indexed: false,
1420 nullable: false,
1421 ..Default::default()
1422 },
1423 );
1424 users_fields.insert(
1425 "email".to_string(),
1426 FieldDefinition {
1427 field_type: FieldType::SCALAR_STRING,
1428 unique: true,
1429 indexed: true,
1430 nullable: false,
1431 ..Default::default()
1432 },
1433 );
1434 users_fields.insert(
1435 "age".to_string(),
1436 FieldDefinition {
1437 field_type: FieldType::SCALAR_INT,
1438 unique: false,
1439 indexed: false,
1440 nullable: false,
1441 ..Default::default()
1442 },
1443 );
1444 users_fields.insert(
1445 "active".to_string(),
1446 FieldDefinition {
1447 field_type: FieldType::SCALAR_BOOL,
1448 unique: false,
1449 indexed: false,
1450 nullable: false,
1451 ..Default::default()
1452 },
1453 );
1454
1455 schema.add_collection(Collection {
1456 name: "users".to_string(),
1457 fields: users_fields,
1458 });
1459
1460 schema
1461 }
1462
1463 #[test]
1464 fn test_validate_unknown_collection() {
1465 let schema = create_test_schema();
1466 let doc = Document {
1467 operations: vec![ast::Operation::Query(Query {
1468 name: None,
1469 variable_definitions: vec![],
1470 directives: vec![],
1471 selection_set: vec![Selection::Field(Field {
1472 alias: None,
1473 name: "nonexistent".to_string(),
1474 arguments: vec![],
1475 directives: vec![],
1476 computed_expression: None,
1477 selection_set: vec![Selection::Field(Field {
1478 alias: None,
1479 name: "id".to_string(),
1480 arguments: vec![],
1481 directives: vec![],
1482 selection_set: vec![],
1483 computed_expression: None,
1484 })],
1485 })],
1486 variables_values: HashMap::new(),
1487 })],
1488 };
1489
1490 let result = validate_document(&doc, &schema, HashMap::new());
1491 assert!(result.is_err());
1492 let errors = result.unwrap_err();
1493 assert!(
1494 errors
1495 .iter()
1496 .any(|e| e.code == ErrorCode::UnknownCollection)
1497 );
1498 }
1499
1500 #[test]
1501 fn test_validate_unknown_field() {
1502 let schema = create_test_schema();
1503 let doc = Document {
1504 operations: vec![ast::Operation::Query(Query {
1505 name: None,
1506 variable_definitions: vec![],
1507 directives: vec![],
1508 selection_set: vec![Selection::Field(Field {
1509 alias: None,
1510 name: "users".to_string(),
1511 arguments: vec![],
1512 directives: vec![],
1513 computed_expression: None,
1514 selection_set: vec![Selection::Field(Field {
1515 alias: None,
1516 name: "nonexistent_field".to_string(),
1517 arguments: vec![],
1518 directives: vec![],
1519 selection_set: vec![],
1520
1521 computed_expression: None,
1522 })],
1523 })],
1524 variables_values: HashMap::new(),
1525 })],
1526 };
1527
1528 let result = validate_document(&doc, &schema, HashMap::new());
1529 assert!(result.is_err());
1530 let errors = result.unwrap_err();
1531 assert!(errors.iter().any(|e| e.code == ErrorCode::UnknownField));
1532 }
1533
1534 #[test]
1535 fn test_validate_missing_required_variable() {
1536 let schema = create_test_schema();
1537 let doc = Document {
1538 operations: vec![ast::Operation::Query(Query {
1539 name: Some("GetUsers".to_string()),
1540 variable_definitions: vec![ast::VariableDefinition {
1541 name: "minAge".to_string(),
1542 var_type: ast::TypeAnnotation {
1543 name: "Int".to_string(),
1544 is_array: false,
1545 is_required: true,
1546 },
1547 default_value: None,
1548 }],
1549 directives: vec![],
1550 selection_set: vec![],
1551 variables_values: HashMap::new(),
1552 })],
1553 };
1554
1555 let result = validate_document(&doc, &schema, HashMap::new());
1557 assert!(result.is_err());
1558 let errors = result.unwrap_err();
1559 assert!(
1560 errors
1561 .iter()
1562 .any(|e| e.code == ErrorCode::MissingRequiredVariable)
1563 );
1564 }
1565
1566 #[test]
1567 fn test_validate_valid_query() {
1568 let schema = create_test_schema();
1569 let doc = Document {
1570 operations: vec![ast::Operation::Query(Query {
1571 name: Some("GetUsers".to_string()),
1572 variable_definitions: vec![],
1573 directives: vec![],
1574 selection_set: vec![Selection::Field(Field {
1575 alias: None,
1576 name: "users".to_string(),
1577 arguments: vec![],
1578 directives: vec![],
1579 computed_expression: None,
1580 selection_set: vec![
1581 Selection::Field(Field {
1582 alias: None,
1583 name: "id".to_string(),
1584 arguments: vec![],
1585 directives: vec![],
1586 selection_set: vec![],
1587
1588 computed_expression: None,
1589 }),
1590 Selection::Field(Field {
1591 alias: None,
1592 name: "name".to_string(),
1593 arguments: vec![],
1594 directives: vec![],
1595 selection_set: vec![],
1596 computed_expression: None,
1597 }),
1598 Selection::Field(Field {
1599 alias: None,
1600 name: "email".to_string(),
1601 arguments: vec![],
1602 directives: vec![],
1603 selection_set: vec![],
1604 computed_expression: None,
1605 }),
1606 ],
1607 })],
1608 variables_values: HashMap::new(),
1609 })],
1610 };
1611
1612 let result = validate_document(&doc, &schema, HashMap::new());
1613 assert!(result.is_ok());
1614 }
1615
1616 #[test]
1617 fn test_validate_filter_type_mismatch() {
1618 let schema = create_test_schema();
1619 let collection = schema.get_collection("users").unwrap();
1620 let mut ctx = ValidationContext::new(&schema);
1621
1622 let filter = Filter::Eq("age".to_string(), Value::String("not a number".to_string()));
1624 validate_filter(&filter, collection, &mut ctx);
1625
1626 assert!(ctx.has_errors());
1627 assert!(ctx.errors.iter().any(|e| e.code == ErrorCode::TypeMismatch));
1628 }
1629
1630 #[test]
1631 fn test_validate_filter_string_operator_on_int() {
1632 let schema = create_test_schema();
1633 let collection = schema.get_collection("users").unwrap();
1634 let mut ctx = ValidationContext::new(&schema);
1635
1636 let filter = Filter::Contains("age".to_string(), Value::String("10".to_string()));
1638 validate_filter(&filter, collection, &mut ctx);
1639
1640 assert!(ctx.has_errors());
1641 assert!(
1642 ctx.errors
1643 .iter()
1644 .any(|e| e.code == ErrorCode::InvalidFilterOperator)
1645 );
1646 }
1647
1648 #[test]
1649 fn test_resolve_variables() {
1650 let mut doc = Document {
1651 operations: vec![ast::Operation::Query(Query {
1652 name: None,
1653 variable_definitions: vec![],
1654 directives: vec![],
1655 selection_set: vec![Selection::Field(Field {
1656 alias: None,
1657 name: "users".to_string(),
1658 arguments: vec![ast::Argument {
1659 name: "limit".to_string(),
1660 value: Value::Variable("pageSize".to_string()),
1661 }],
1662 directives: vec![],
1663 selection_set: vec![],
1664 computed_expression: None,
1665 })],
1666 variables_values: HashMap::new(),
1667 })],
1668 };
1669
1670 let mut vars = HashMap::new();
1671 vars.insert("pageSize".to_string(), Value::Int(10));
1672
1673 let result = resolve_variables(&mut doc, &vars);
1674 assert!(result.is_ok());
1675
1676 if let ast::Operation::Query(query) = &doc.operations[0] {
1678 if let Selection::Field(user_field) = &query.selection_set[0] {
1679 let arg = &user_field.arguments[0];
1680 assert!(matches!(arg.value, Value::Int(10)));
1681 } else {
1682 panic!("Expected Selection::Field");
1683 }
1684 } else {
1685 panic!("Expected Query operation");
1686 }
1687 }
1688}