Repository: apollographql/apollo-rs
Pull request #250 (2022-06-10 20:58:40)

feat(apollo-compiler): add input object definitions to source database
- adds input object values
- adds validation for unique input object definition and unique input fields in input object

related to #247 


Patch:
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index dfb7617..e5937e5 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -76,6 +76,10 @@ impl ApolloCompiler {
     pub fn directive_definitions(&self) -> Arc<Vec<values::DirectiveDefinition>> {
         self.db.directive_definitions()
     }
+
+    pub fn input_objects(&self) -> Arc<Vec<values::InputObjectDefinition>> {
+        self.db.input_objects()
+    }
 }
 
 #[cfg(test)]
@@ -351,6 +355,46 @@ type Book @delegateField(name: "pageCount") @delegateField(name: "author") {
             .flatten()
             .collect();
 
-        assert_eq!(["OBJECT", "INTERFACE"], locations.as_ref());
+        assert_eq!(locations, ["OBJECT", "INTERFACE"]);
+    }
+
+    #[test]
+    fn it_accesses_input_object_definitions() {
+        let input = r#"
+type Query {
+  point1: Point2D
+  point2: Point2D
+}
+
+input Point2D {
+  x: Float
+  y: Float
+}
+"#;
+
+        let ctx = ApolloCompiler::new(input);
+        let errors = ctx.validate();
+
+        assert!(errors.is_empty());
+
+        let input_objects = ctx.input_objects();
+        let fields: Vec<&str> = input_objects
+            .iter()
+            .filter_map(|input| {
+                if input.name() == "Point2D" {
+                    let fields: Vec<&str> = input
+                        .input_fields_definition()
+                        .iter()
+                        .map(|val| val.name())
+                        .collect();
+                    Some(fields)
+                } else {
+                    None
+                }
+            })
+            .flatten()
+            .collect();
+
+        assert_eq!(fields, ["x", "y"]);
     }
 }
diff --git a/crates/apollo-compiler/src/queries/database.rs b/crates/apollo-compiler/src/queries/database.rs
index 5c198ac..bb31611 100644
--- a/crates/apollo-compiler/src/queries/database.rs
+++ b/crates/apollo-compiler/src/queries/database.rs
@@ -54,6 +54,8 @@ pub trait SourceDatabase {
 
     fn directive_definitions(&self) -> Arc<Vec<DirectiveDefinition>>;
 
+    fn input_objects(&self) -> Arc<Vec<InputObjectDefinition>>;
+
     fn query_operations(&self) -> Operations;
 
     fn mutation_operations(&self) -> Operations;
@@ -495,6 +497,21 @@ fn find_directive_definition_by_name(
     })
 }
 
+fn input_objects(db: &dyn SourceDatabase) -> Arc<Vec<InputObjectDefinition>> {
+    let directives = db
+        .definitions()
+        .iter()
+        .filter_map(|definition| match definition {
+            ast::Definition::InputObjectTypeDefinition(input_obj) => {
+                Some(input_object_definition(input_obj.clone()))
+            }
+            _ => None,
+        })
+        .collect();
+
+    Arc::new(directives)
+}
+
 fn operation_definition(
     db: &dyn SourceDatabase,
     op_def: ast::OperationDefinition,
@@ -715,6 +732,22 @@ fn directive_definition(directive_def: ast::DirectiveDefinition) -> DirectiveDef
     }
 }
 
+fn input_object_definition(input_obj: ast::InputObjectTypeDefinition) -> InputObjectDefinition {
+    let id = Uuid::new_v4();
+    let description = description(input_obj.description());
+    let name = name(input_obj.name());
+    let directives = directives(input_obj.directives());
+    let input_fields_definition = input_fields_definition(input_obj.input_fields_definition());
+
+    InputObjectDefinition {
+        id,
+        description,
+        name,
+        directives,
+        input_fields_definition,
+    }
+}
+
 fn add_object_type_id_to_schema(db: &dyn SourceDatabase) -> Arc<Vec<RootOperationTypeDefinition>> {
     // Schema Definition does not have to be present in the SDL if ObjectType name is
     // - Query
@@ -808,6 +841,15 @@ fn arguments_definition(
     }
 }
 
+fn input_fields_definition(
+    input_fields: Option<ast::InputFieldsDefinition>,
+) -> Arc<Vec<InputValueDefinition>> {
+    match input_fields {
+        Some(fields) => input_value_definitions(fields.input_value_definitions()),
+        None => Arc::new(Vec::new()),
+    }
+}
+
 fn input_value_definitions(
     input_values: AstChildren<ast::InputValueDefinition>,
 ) -> Arc<Vec<InputValueDefinition>> {
diff --git a/crates/apollo-compiler/src/queries/values.rs b/crates/apollo-compiler/src/queries/values.rs
index 9851a8f..87c72da 100644
--- a/crates/apollo-compiler/src/queries/values.rs
+++ b/crates/apollo-compiler/src/queries/values.rs
@@ -874,6 +874,11 @@ pub struct InputValueDefinition {
 }
 
 impl InputValueDefinition {
+    // Get a reference to input value definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
     // Get a reference to input value definition's directives.
     pub fn directives(&self) -> &[Directive] {
         self.directives.as_ref()
@@ -1010,3 +1015,33 @@ impl InterfaceDefinition {
         self.fields_definition.as_ref()
     }
 }
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct InputObjectDefinition {
+    pub(crate) id: Uuid,
+    pub(crate) description: Option<String>,
+    pub(crate) name: String,
+    pub(crate) directives: Arc<Vec<Directive>>,
+    pub(crate) input_fields_definition: Arc<Vec<InputValueDefinition>>,
+}
+
+impl InputObjectDefinition {
+    /// Get the input object definition's id.
+    pub fn id(&self) -> &Uuid {
+        &self.id
+    }
+
+    /// Get a reference to the input object definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    /// Get a reference to input object definition's directives.
+    pub fn directives(&self) -> &[Directive] {
+        self.directives.as_ref()
+    }
+
+    pub fn input_fields_definition(&self) -> &[InputValueDefinition] {
+        self.input_fields_definition.as_ref()
+    }
+}
diff --git a/crates/apollo-compiler/src/validation/input_object.rs b/crates/apollo-compiler/src/validation/input_object.rs
new file mode 100644
index 0000000..1e57bf7
--- /dev/null
+++ b/crates/apollo-compiler/src/validation/input_object.rs
@@ -0,0 +1,45 @@
+use std::collections::HashSet;
+
+use crate::{diagnostics::ErrorDiagnostic, ApolloDiagnostic, SourceDatabase};
+
+pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
+    let mut errors = Vec::new();
+
+    // Input Object Definitions must have unique names.
+    //
+    // Return a Unique Definition error in case of a duplicate name.
+    let mut seen = HashSet::new();
+    for input_objects in db.input_objects().iter() {
+        let name = input_objects.name();
+        if seen.contains(name) {
+            errors.push(ApolloDiagnostic::Error(ErrorDiagnostic::UniqueDefinition {
+                message: "Input Object Definitions must have unique names".into(),
+                definition: name.to_string(),
+            }));
+        } else {
+            seen.insert(name);
+        }
+    }
+
+    // Fields in an Input Object Definition must be unique
+    //
+    // Returns Unique Value error.
+    for input_objects in db.input_objects().iter() {
+        let mut seen = HashSet::new();
+
+        let input_fields = input_objects.input_fields_definition();
+
+        for field in input_fields {
+            let field_name = field.name();
+            if seen.contains(&field_name) {
+                errors.push(ApolloDiagnostic::Error(ErrorDiagnostic::UniqueValue {
+                    message: "Input Fields must be unique".into(),
+                    value: field_name.into(),
+                }));
+            } else {
+                seen.insert(field_name);
+            }
+        }
+    }
+    errors
+}
diff --git a/crates/apollo-compiler/src/validation/interfaces.rs b/crates/apollo-compiler/src/validation/interfaces.rs
index 0c7843c..2de7700 100644
--- a/crates/apollo-compiler/src/validation/interfaces.rs
+++ b/crates/apollo-compiler/src/validation/interfaces.rs
@@ -53,6 +53,8 @@ pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
     }
 
     // Fields in an Interface definition must be unique
+    //
+    // Returns Unique Value error.
     for interface_def in db.interfaces().iter() {
         let mut seen = HashSet::new();
 
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 2f112de..9815527 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -10,6 +10,7 @@ pub mod unions;
 
 // composite nodes
 pub mod directives;
+pub mod input_object;
 pub mod interfaces;
 
 // executable definitions
@@ -42,6 +43,7 @@ impl<'a> Validator<'a> {
 
         self.errors.extend(interfaces::check(self.db));
         self.errors.extend(directives::check(self.db));
+        self.errors.extend(input_object::check(self.db));
 
         self.errors.extend(operations::check(self.db));
         self.errors.extend(unused_variables::check(self.db));


Test patch:
diff --git a/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.graphql b/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.graphql
new file mode 100644
index 0000000..5a2e5b4
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.graphql
@@ -0,0 +1,14 @@
+type Query {
+  point1: Point2D
+  point2: Point2D
+}
+
+input Point2D {
+  x: Float
+  y: Float
+}
+
+input Point2D {
+  x: Float
+  y: Float
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.txt b/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.txt
new file mode 100644
index 0000000..c479cd6
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0030_schema_with_duplicate_input_objects.txt
@@ -0,0 +1,1 @@
+[Error(UniqueDefinition { message: "Input Object Definitions must have unique names", definition: "Point2D" })]
diff --git a/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.graphql b/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.graphql
new file mode 100644
index 0000000..10c57eb
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.graphql
@@ -0,0 +1,9 @@
+type Query {
+  point1: Point2D
+  point2: Point2D
+}
+
+input Point2D {
+  x: Float
+  x: Float
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.txt b/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.txt
new file mode 100644
index 0000000..43cd4c2
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0031_input_object_with_duplicate_fields.txt
@@ -0,0 +1,1 @@
+[Error(UniqueValue { message: "Input Fields must be unique", value: "x" })]
diff --git a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.graphql b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.graphql
new file mode 100644
index 0000000..d3258ff
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.graphql
@@ -0,0 +1,9 @@
+type Query {
+  point1: Point2D
+  point2: Point2D
+}
+
+input Point2D {
+  x: Float
+  y: Float
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt
new file mode 100644
index 0000000..b0345f7
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt
@@ -0,0 +1,59 @@
+- DOCUMENT@0..91
+    - OBJECT_TYPE_DEFINITION@0..52
+        - type_KW@0..4 "type"
+        - WHITESPACE@4..5 " "
+        - NAME@5..11
+            - IDENT@5..10 "Query"
+            - WHITESPACE@10..11 " "
+        - FIELDS_DEFINITION@11..52
+            - L_CURLY@11..12 "{"
+            - WHITESPACE@12..15 "\n  "
+            - FIELD_DEFINITION@15..33
+                - NAME@15..21
+                    - IDENT@15..21 "point1"
+                - COLON@21..22 ":"
+                - WHITESPACE@22..23 " "
+                - NAMED_TYPE@23..30
+                    - NAME@23..30
+                        - IDENT@23..30 "Point2D"
+                - WHITESPACE@30..33 "\n  "
+            - FIELD_DEFINITION@33..49
+                - NAME@33..39
+                    - IDENT@33..39 "point2"
+                - COLON@39..40 ":"
+                - WHITESPACE@40..41 " "
+                - NAMED_TYPE@41..48
+                    - NAME@41..48
+                        - IDENT@41..48 "Point2D"
+                - WHITESPACE@48..49 "\n"
+            - R_CURLY@49..50 "}"
+            - WHITESPACE@50..52 "\n\n"
+    - INPUT_OBJECT_TYPE_DEFINITION@52..91
+        - input_KW@52..57 "input"
+        - WHITESPACE@57..58 " "
+        - NAME@58..66
+            - IDENT@58..65 "Point2D"
+            - WHITESPACE@65..66 " "
+        - INPUT_FIELDS_DEFINITION@66..91
+            - L_CURLY@66..67 "{"
+            - WHITESPACE@67..70 "\n  "
+            - INPUT_VALUE_DEFINITION@70..81
+                - NAME@70..71
+                    - IDENT@70..71 "x"
+                - COLON@71..72 ":"
+                - WHITESPACE@72..73 " "
+                - NAMED_TYPE@73..78
+                    - NAME@73..78
+                        - IDENT@73..78 "Float"
+                - WHITESPACE@78..81 "\n  "
+            - INPUT_VALUE_DEFINITION@81..90
+                - NAME@81..82
+                    - IDENT@81..82 "y"
+                - COLON@82..83 ":"
+                - WHITESPACE@83..84 " "
+                - NAMED_TYPE@84..89
+                    - NAME@84..89
+                        - IDENT@84..89 "Float"
+                - WHITESPACE@89..90 "\n"
+            - R_CURLY@90..91 "}"
+recursion limit: 4096, high: 0