Repository: apollographql/apollo-rs
Pull request #238 (2022-05-30 16:12:32)

feat(apollo-compiler): add union definitions to source database
- adds union definitions and union members to source database
- adds validation for unique union member types
- adds a UUID reference to object type for union members
- adds a getter for object type's fields_definition
- adds a getter for field definition name

closes #238 


Patch:
diff --git a/crates/apollo-compiler/src/lib.rs b/crates/apollo-compiler/src/lib.rs
index 6808176..f3dadfe 100644
--- a/crates/apollo-compiler/src/lib.rs
+++ b/crates/apollo-compiler/src/lib.rs
@@ -68,6 +68,10 @@ impl ApolloCompiler {
     pub fn enums(&self) -> Arc<Vec<values::EnumDefinition>> {
         self.db.enums()
     }
+
+    pub fn unions(&self) -> Arc<Vec<values::UnionDefinition>> {
+        self.db.unions()
+    }
 }
 
 #[cfg(test)]
@@ -244,4 +248,65 @@ enum Pet {
             .collect();
         assert_eq!(enum_values, ["CAT", "DOG", "FOX"]);
     }
+
+    #[test]
+    fn it_accesses_union_definitions() {
+        let input = r#"
+schema {
+  query: SearchQuery
+}
+
+union SearchResult = Photo | Person
+
+type Person {
+  name: String
+  age: Int
+}
+
+type Photo {
+  height: Int
+  width: Int
+}
+
+type SearchQuery {
+  firstSearchResult: SearchResult
+}
+"#;
+
+        let ctx = ApolloCompiler::new(input);
+        let errors = ctx.validate();
+
+        assert!(errors.is_empty());
+
+        let unions = ctx.unions();
+
+        let union_members: Vec<&str> = unions
+            .iter()
+            .find(|def| def.name() == "SearchResult")
+            .unwrap()
+            .union_members()
+            .iter()
+            .map(|member| member.name())
+            .collect();
+        assert_eq!(union_members, ["Photo", "Person"]);
+
+        let photo_object = unions
+            .iter()
+            .find(|def| def.name() == "SearchResult")
+            .unwrap()
+            .union_members()
+            .iter()
+            .find(|mem| mem.name() == "Person")
+            .unwrap()
+            .object(&ctx.db);
+
+        if let Some(photo) = photo_object {
+            let fields: Vec<&str> = photo
+                .fields_definition()
+                .iter()
+                .map(|field| field.name())
+                .collect();
+            assert_eq!(fields, ["name", "age"])
+        }
+    }
 }
diff --git a/crates/apollo-compiler/src/queries/database.rs b/crates/apollo-compiler/src/queries/database.rs
index 4d1215c..810755a 100644
--- a/crates/apollo-compiler/src/queries/database.rs
+++ b/crates/apollo-compiler/src/queries/database.rs
@@ -48,6 +48,8 @@ pub trait SourceDatabase {
 
     fn enums(&self) -> Arc<Vec<EnumDefinition>>;
 
+    fn unions(&self) -> Arc<Vec<UnionDefinition>>;
+
     fn query_operations(&self) -> Operations;
 
     fn mutation_operations(&self) -> Operations;
@@ -370,6 +372,20 @@ fn enums(db: &dyn SourceDatabase) -> Arc<Vec<EnumDefinition>> {
     Arc::new(enums)
 }
 
+fn unions(db: &dyn SourceDatabase) -> Arc<Vec<UnionDefinition>> {
+    let unions = db
+        .definitions()
+        .iter()
+        .filter_map(|definition| match definition {
+            ast::Definition::UnionTypeDefinition(union_def) => {
+                Some(union_definition(db, union_def.clone()))
+            }
+            _ => None,
+        })
+        .collect();
+    Arc::new(unions)
+}
+
 fn find_object_type(db: &dyn SourceDatabase, id: Uuid) -> Option<Arc<ObjectTypeDefinition>> {
     db.object_types().iter().find_map(|object_type| {
         if &id == object_type.id() {
@@ -533,6 +549,49 @@ fn enum_value_definition(enum_value_def: ast::EnumValueDefinition) -> EnumValueD
     }
 }
 
+fn union_definition(
+    db: &dyn SourceDatabase,
+    union_def: ast::UnionTypeDefinition,
+) -> UnionDefinition {
+    let description = description(union_def.description());
+    let name = name(union_def.name());
+    let directives = directives(union_def.directives());
+    let union_members = union_members(db, union_def.union_member_types());
+
+    UnionDefinition {
+        description,
+        name,
+        directives,
+        union_members,
+    }
+}
+
+fn union_members(
+    db: &dyn SourceDatabase,
+    union_members: Option<ast::UnionMemberTypes>,
+) -> Arc<Vec<UnionMember>> {
+    match union_members {
+        Some(members) => {
+            let mems = members
+                .named_types()
+                .into_iter()
+                .map(|u| union_member(db, u))
+                .collect();
+            Arc::new(mems)
+        }
+        None => Arc::new(Vec::new()),
+    }
+}
+
+fn union_member(db: &dyn SourceDatabase, member: ast::NamedType) -> UnionMember {
+    let name = name(member.name());
+    let object_id = db
+        .find_object_type_by_name(name.clone())
+        .map(|object_type| *object_type.id());
+
+    UnionMember { name, object_id }
+}
+
 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
diff --git a/crates/apollo-compiler/src/queries/values.rs b/crates/apollo-compiler/src/queries/values.rs
index 0eadf4c..0f29455 100644
--- a/crates/apollo-compiler/src/queries/values.rs
+++ b/crates/apollo-compiler/src/queries/values.rs
@@ -665,15 +665,20 @@ pub struct ObjectTypeDefinition {
 }
 
 impl ObjectTypeDefinition {
-    /// get the object type definition's id.
+    /// Get the object type definition's id.
     pub fn id(&self) -> &Uuid {
         &self.id
     }
 
-    /// get a reference to the object type definition's name.
+    /// Get a reference to the object type definition's name.
     pub fn name(&self) -> &str {
         self.name.as_ref()
     }
+
+    /// Get a reference to the object type definition's field definitions.
+    pub fn fields_definition(&self) -> &[FieldDefinition] {
+        self.fields_definition.as_ref()
+    }
 }
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
@@ -690,6 +695,13 @@ pub struct FieldDefinition {
     pub(crate) directives: Arc<Vec<Directive>>,
 }
 
+impl FieldDefinition {
+    /// Get a reference to the field definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 pub struct ArgumentsDefinition {
     pub(crate) input_values: Arc<Vec<InputValueDefinition>>,
@@ -760,3 +772,45 @@ impl EnumValueDefinition {
         self.enum_value.as_ref()
     }
 }
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct UnionDefinition {
+    pub(crate) description: Option<String>,
+    pub(crate) name: String,
+    pub(crate) directives: Arc<Vec<Directive>>,
+    pub(crate) union_members: Arc<Vec<UnionMember>>,
+}
+
+impl UnionDefinition {
+    /// Get a reference to the union definition's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    /// Get a reference to union definition's directives.
+    pub fn directives(&self) -> &[Directive] {
+        self.directives.as_ref()
+    }
+
+    /// Get a reference to union definition's union members.
+    pub fn union_members(&self) -> &[UnionMember] {
+        self.union_members.as_ref()
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct UnionMember {
+    pub(crate) name: String,
+    pub(crate) object_id: Option<Uuid>,
+}
+
+impl UnionMember {
+    /// Get a reference to the union member's name.
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    pub fn object(&self, db: &dyn SourceDatabase) -> Option<Arc<ObjectTypeDefinition>> {
+        db.find_object_type(self.object_id?)
+    }
+}
diff --git a/crates/apollo-compiler/src/validation/enums.rs b/crates/apollo-compiler/src/validation/enums.rs
index 8cd2e6e..a4d9c43 100644
--- a/crates/apollo-compiler/src/validation/enums.rs
+++ b/crates/apollo-compiler/src/validation/enums.rs
@@ -10,7 +10,7 @@ pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
 
     // An Enum type must define one or more unique enum values.
     //
-    // Return a Unique Operation Definition error in case of a duplicate name.
+    // Return a Unique Value error in case of a duplicate value.
     for enum_def in db.enums().iter() {
         let mut seen = HashSet::new();
         for enum_value in enum_def.enum_values_definition().iter() {
diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs
index 8d9b76d..0a4d66e 100644
--- a/crates/apollo-compiler/src/validation/mod.rs
+++ b/crates/apollo-compiler/src/validation/mod.rs
@@ -6,6 +6,7 @@ pub mod schema;
 // leaf nodes
 pub mod enums;
 pub mod scalars;
+pub mod unions;
 
 // executable definitions
 pub mod operations;
@@ -28,11 +29,16 @@ impl<'a> Validator<'a> {
 
     pub fn validate(&mut self) -> &mut [ApolloDiagnostic] {
         self.errors.extend(self.db.syntax_errors());
+
         self.errors.extend(schema::check(self.db));
+
         self.errors.extend(scalars::check(self.db));
         self.errors.extend(enums::check(self.db));
+        self.errors.extend(unions::check(self.db));
+
         self.errors.extend(operations::check(self.db));
         self.errors.extend(unused_variables::check(self.db));
+
         self.errors.as_mut()
     }
 }
diff --git a/crates/apollo-compiler/src/validation/unions.rs b/crates/apollo-compiler/src/validation/unions.rs
new file mode 100644
index 0000000..b69419d
--- /dev/null
+++ b/crates/apollo-compiler/src/validation/unions.rs
@@ -0,0 +1,27 @@
+use std::collections::HashSet;
+
+use crate::{diagnostics::ErrorDiagnostic, ApolloDiagnostic, SourceDatabase};
+
+pub fn check(db: &dyn SourceDatabase) -> Vec<ApolloDiagnostic> {
+    let mut errors = Vec::new();
+
+    // A Union type must include one or more unique member types.
+    //
+    // Return a Unique Value error in case of a duplicate member.
+    for union_def in db.unions().iter() {
+        let mut seen = HashSet::new();
+        for union_member in union_def.union_members().iter() {
+            let member = union_member.name();
+            if seen.contains(&member) {
+                errors.push(ApolloDiagnostic::Error(ErrorDiagnostic::UniqueValue {
+                    message: "Union member types must be unique".into(),
+                    value: member.into(),
+                }));
+            } else {
+                seen.insert(member);
+            }
+        }
+    }
+
+    errors
+}


Test patch:
diff --git a/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.graphql b/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.graphql
new file mode 100644
index 0000000..0719106
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.graphql
@@ -0,0 +1,19 @@
+schema {
+  query: SearchQuery
+}
+
+union SearchResult = Photo | Photo
+
+type Person {
+  name: String
+  age: Int
+}
+
+type Photo {
+  height: Int
+  width: Int
+}
+
+type SearchQuery {
+  firstSearchResult: SearchResult
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.txt b/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.txt
new file mode 100644
index 0000000..c89524e
--- /dev/null
+++ b/crates/apollo-compiler/test_data/err/0021_union_definition_with_duplicate_members.txt
@@ -0,0 +1,1 @@
+[Error(UniqueValue { message: "Union member types must be unique", value: "Photo" })]
diff --git a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.graphql b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.graphql
new file mode 100644
index 0000000..40036f9
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.graphql
@@ -0,0 +1,19 @@
+schema {
+  query: SearchQuery
+}
+
+union SearchResult = Photo | Person
+
+type Person {
+  name: String
+  age: Int
+}
+
+type Photo {
+  height: Int
+  width: Int
+}
+
+type SearchQuery {
+  firstSearchResult: SearchResult
+}
\ No newline at end of file
diff --git a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt
new file mode 100644
index 0000000..f0f021d
--- /dev/null
+++ b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt
@@ -0,0 +1,114 @@
+- DOCUMENT@0..210
+    - SCHEMA_DEFINITION@0..33
+        - schema_KW@0..6 "schema"
+        - WHITESPACE@6..7 " "
+        - L_CURLY@7..8 "{"
+        - WHITESPACE@8..11 "\n  "
+        - ROOT_OPERATION_TYPE_DEFINITION@11..30
+            - OPERATION_TYPE@11..16
+                - query_KW@11..16 "query"
+            - COLON@16..17 ":"
+            - WHITESPACE@17..18 " "
+            - NAMED_TYPE@18..30
+                - NAME@18..30
+                    - IDENT@18..29 "SearchQuery"
+                    - WHITESPACE@29..30 "\n"
+        - R_CURLY@30..31 "}"
+        - WHITESPACE@31..33 "\n\n"
+    - UNION_TYPE_DEFINITION@33..70
+        - union_KW@33..38 "union"
+        - WHITESPACE@38..39 " "
+        - NAME@39..52
+            - IDENT@39..51 "SearchResult"
+            - WHITESPACE@51..52 " "
+        - UNION_MEMBER_TYPES@52..70
+            - EQ@52..53 "="
+            - WHITESPACE@53..54 " "
+            - NAMED_TYPE@54..60
+                - NAME@54..60
+                    - IDENT@54..59 "Photo"
+                    - WHITESPACE@59..60 " "
+            - PIPE@60..61 "|"
+            - WHITESPACE@61..62 " "
+            - NAMED_TYPE@62..70
+                - NAME@62..70
+                    - IDENT@62..68 "Person"
+                    - WHITESPACE@68..70 "\n\n"
+    - OBJECT_TYPE_DEFINITION@70..113
+        - type_KW@70..74 "type"
+        - WHITESPACE@74..75 " "
+        - NAME@75..82
+            - IDENT@75..81 "Person"
+            - WHITESPACE@81..82 " "
+        - FIELDS_DEFINITION@82..113
+            - L_CURLY@82..83 "{"
+            - WHITESPACE@83..86 "\n  "
+            - FIELD_DEFINITION@86..101
+                - NAME@86..90
+                    - IDENT@86..90 "name"
+                - COLON@90..91 ":"
+                - WHITESPACE@91..92 " "
+                - NAMED_TYPE@92..98
+                    - NAME@92..98
+                        - IDENT@92..98 "String"
+                - WHITESPACE@98..101 "\n  "
+            - FIELD_DEFINITION@101..110
+                - NAME@101..104
+                    - IDENT@101..104 "age"
+                - COLON@104..105 ":"
+                - WHITESPACE@105..106 " "
+                - NAMED_TYPE@106..109
+                    - NAME@106..109
+                        - IDENT@106..109 "Int"
+                - WHITESPACE@109..110 "\n"
+            - R_CURLY@110..111 "}"
+            - WHITESPACE@111..113 "\n\n"
+    - OBJECT_TYPE_DEFINITION@113..156
+        - type_KW@113..117 "type"
+        - WHITESPACE@117..118 " "
+        - NAME@118..124
+            - IDENT@118..123 "Photo"
+            - WHITESPACE@123..124 " "
+        - FIELDS_DEFINITION@124..156
+            - L_CURLY@124..125 "{"
+            - WHITESPACE@125..128 "\n  "
+            - FIELD_DEFINITION@128..142
+                - NAME@128..134
+                    - IDENT@128..134 "height"
+                - COLON@134..135 ":"
+                - WHITESPACE@135..136 " "
+                - NAMED_TYPE@136..139
+                    - NAME@136..139
+                        - IDENT@136..139 "Int"
+                - WHITESPACE@139..142 "\n  "
+            - FIELD_DEFINITION@142..153
+                - NAME@142..147
+                    - IDENT@142..147 "width"
+                - COLON@147..148 ":"
+                - WHITESPACE@148..149 " "
+                - NAMED_TYPE@149..152
+                    - NAME@149..152
+                        - IDENT@149..152 "Int"
+                - WHITESPACE@152..153 "\n"
+            - R_CURLY@153..154 "}"
+            - WHITESPACE@154..156 "\n\n"
+    - OBJECT_TYPE_DEFINITION@156..210
+        - type_KW@156..160 "type"
+        - WHITESPACE@160..161 " "
+        - NAME@161..173
+            - IDENT@161..172 "SearchQuery"
+            - WHITESPACE@172..173 " "
+        - FIELDS_DEFINITION@173..210
+            - L_CURLY@173..174 "{"
+            - WHITESPACE@174..177 "\n  "
+            - FIELD_DEFINITION@177..209
+                - NAME@177..194
+                    - IDENT@177..194 "firstSearchResult"
+                - COLON@194..195 ":"
+                - WHITESPACE@195..196 " "
+                - NAMED_TYPE@196..208
+                    - NAME@196..208
+                        - IDENT@196..208 "SearchResult"
+                - WHITESPACE@208..209 "\n"
+            - R_CURLY@209..210 "}"
+