Repository: apollographql/apollo-rs (rust)

Issue:
validation: union extensions
* [x] The named type must already be defined
* [x] the already defined type must be a Union type.
* [ ] The member types of a Union type extension must all be Object base types; Scalar, Interface and Union types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union.
* [ ] All member types of a Union type extension must be unique.
* [ ] All member types of a Union type extension must not already be a member of the original Union type.
* [ ] Any non-repeatable directives provided must not already apply to the original Union type.


[spec](https://spec.graphql.org/October2021/#sec-Union-Extensions.Type-Validation)

Pull request:
feat(compiler): validate union extensions, fixes #478
a very simple one 😁 

Patch:
diff --git a/crates/apollo-compiler/src/validation/union_.rs b/crates/apollo-compiler/src/validation/union_.rs
index 74bb17a..3462943 100644
--- a/crates/apollo-compiler/src/validation/union_.rs
+++ b/crates/apollo-compiler/src/validation/union_.rs
@@ -17,6 +17,15 @@ pub fn validate_union_definitions(db: &dyn ValidationDatabase) -> Vec<ApolloDiag
     diagnostics
 }
 
+fn iter_with_extensions<'a, Item, Ext>(
+    base: &'a [Item],
+    extensions: &'a [Arc<Ext>],
+    method: impl Fn(&'a Ext) -> &'a [Item],
+) -> impl Iterator<Item = &'a Item> {
+    base.iter()
+        .chain(extensions.iter().flat_map(move |ext| method(ext).iter()))
+}
+
 pub fn validate_union_definition(
     db: &dyn ValidationDatabase,
     union_def: Arc<hir::UnionTypeDefinition>,
@@ -26,8 +35,13 @@ pub fn validate_union_definition(
         hir::DirectiveLocation::Union,
     );
 
+    let union_members = iter_with_extensions(
+        union_def.self_members(),
+        union_def.extensions(),
+        hir::UnionTypeExtension::union_members,
+    );
     let mut seen: HashMap<&str, &UnionMember> = HashMap::new();
-    for union_member in union_def.self_members().iter() {
+    for union_member in union_members {
         let name = union_member.name();
         let redefined_definition = union_member.loc();
         // A Union type must include one or more unique member types.


Test patch:
diff --git a/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.graphql b/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.graphql
new file mode 100644
index 0000000..7e4b8ce
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.graphql
@@ -0,0 +1,17 @@
+scalar ThisIsAScalar @specifiedBy(url: "https://example.com")
+
+type WithFieldA { a: String }
+type WithFieldB { b: String }
+
+# Extension can't add a non-object type
+union NonObjectType = WithFieldA | WithFieldB
+extend union NonObjectType = ThisIsAScalar
+
+# Extension can't add duplicate members
+union DuplicateMembers = WithFieldA
+extend union DuplicateMembers = WithFieldA | WithFieldB
+
+# Extension can't add duplicate directives
+directive @nonRepeatable on UNION
+union DuplicateDirective @nonRepeatable = WithFieldA
+extend union DuplicateDirective @nonRepeatable = WithFieldB
diff --git a/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.txt b/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.txt
new file mode 100644
index 0000000..a794349
--- /dev/null
+++ b/crates/apollo-compiler/test_data/diagnostics/0100_union_extensions.txt
@@ -0,0 +1,143 @@
+[
+    ApolloDiagnostic {
+        cache: {
+            0: "built_in_types.graphql",
+            97: "0100_union_extensions.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 97,
+            },
+            offset: 239,
+            length: 13,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 97,
+                    },
+                    offset: 239,
+                    length: 13,
+                },
+                text: "This is of `ScalarTypeDefinition` type",
+            },
+        ],
+        help: Some(
+            "Union members must be of base Object Type.",
+        ),
+        data: ObjectType {
+            name: "ThisIsAScalar",
+            ty: "ScalarTypeDefinition",
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            0: "built_in_types.graphql",
+            97: "0100_union_extensions.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 97,
+            },
+            offset: 362,
+            length: 10,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 97,
+                    },
+                    offset: 319,
+                    length: 10,
+                },
+                text: "previous definition of `WithFieldA` here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 97,
+                    },
+                    offset: 362,
+                    length: 10,
+                },
+                text: "`WithFieldA` redefined here",
+            },
+        ],
+        help: Some(
+            "`WithFieldA` must only be defined once in this document.",
+        ),
+        data: UniqueDefinition {
+            ty: "union member",
+            name: "WithFieldA",
+            original_definition: DiagnosticLocation {
+                file_id: FileId {
+                    id: 97,
+                },
+                offset: 319,
+                length: 10,
+            },
+            redefined_definition: DiagnosticLocation {
+                file_id: FileId {
+                    id: 97,
+                },
+                offset: 362,
+                length: 10,
+            },
+        },
+    },
+    ApolloDiagnostic {
+        cache: {
+            0: "built_in_types.graphql",
+            97: "0100_union_extensions.graphql",
+        },
+        location: DiagnosticLocation {
+            file_id: FileId {
+                id: 97,
+            },
+            offset: 549,
+            length: 14,
+        },
+        labels: [
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 97,
+                    },
+                    offset: 489,
+                    length: 14,
+                },
+                text: "directive nonRepeatable first called here",
+            },
+            Label {
+                location: DiagnosticLocation {
+                    file_id: FileId {
+                        id: 97,
+                    },
+                    offset: 549,
+                    length: 14,
+                },
+                text: "directive nonRepeatable called again here",
+            },
+        ],
+        help: None,
+        data: UniqueDirective {
+            name: "nonRepeatable",
+            original_call: DiagnosticLocation {
+                file_id: FileId {
+                    id: 97,
+                },
+                offset: 489,
+                length: 14,
+            },
+            conflicting_call: DiagnosticLocation {
+                file_id: FileId {
+                    id: 97,
+                },
+                offset: 549,
+                length: 14,
+            },
+        },
+    },
+]