Skip to content

Commit 986b79a

Browse files
committed
allow unions to declare implementation of interfaces
WIP: more tests required complete code coverage is already there, but goal is to have a test where union implements an interface wherever there is a test for an interface implementing interface
1 parent c7d7026 commit 986b79a

31 files changed

+827
-42
lines changed

src/__testUtils__/kitchenSinkSDL.ts

+22
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ extend union Feed = Photo | Video
7979
8080
extend union Feed @onUnion
8181
82+
interface Node {
83+
id: ID
84+
}
85+
86+
interface Resource {
87+
url: String
88+
}
89+
90+
extend type Photo implements Node {
91+
id: ID
92+
url: String
93+
}
94+
95+
extend type Video implements Node {
96+
id: ID
97+
url: String
98+
}
99+
100+
union Media implements Node = Photo | Video
101+
102+
extend union Media implements Resource
103+
82104
scalar CustomScalar
83105
84106
scalar AnnotatedScalar @onScalar

src/execution/__tests__/union-interface-test.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ const CatType: GraphQLObjectType = new GraphQLObjectType({
110110

111111
const PetType = new GraphQLUnionType({
112112
name: 'Pet',
113+
interfaces: [MammalType, LifeType, NamedType],
113114
types: [DogType, CatType],
114115
resolveType(value) {
115116
if (value instanceof Dog) {
@@ -211,8 +212,13 @@ describe('Execute: Union and intersection types', () => {
211212
Pet: {
212213
kind: 'UNION',
213214
name: 'Pet',
214-
fields: null,
215-
interfaces: null,
215+
fields: [
216+
{ name: 'progeny' },
217+
{ name: 'mother' },
218+
{ name: 'father' },
219+
{ name: 'name' },
220+
],
221+
interfaces: [{ name: 'Mammal' }, { name: 'Life' }, { name: 'Named' }],
216222
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
217223
enumValues: null,
218224
inputFields: null,
@@ -264,12 +270,11 @@ describe('Execute: Union and intersection types', () => {
264270
name
265271
pets {
266272
__typename
273+
name
267274
... on Dog {
268-
name
269275
barks
270276
}
271277
... on Cat {
272-
name
273278
meows
274279
}
275280
}
@@ -436,12 +441,11 @@ describe('Execute: Union and intersection types', () => {
436441
437442
fragment PetFields on Pet {
438443
__typename
444+
name
439445
... on Dog {
440-
name
441446
barks
442447
}
443448
... on Cat {
444-
name
445449
meows
446450
}
447451
}

src/jsutils/mapValues.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { ObjMap, ReadOnlyObjMap } from './ObjMap';
2+
3+
/**
4+
* Creates an object map from an array of `maps` with the same keys as each `map`
5+
* in `maps` and values generated by running each value of `map` thru `fn`.
6+
*/
7+
export function mapValues<T, V>(
8+
maps: ReadonlyArray<ReadOnlyObjMap<T>>,
9+
fn: (value: T, key: string) => V,
10+
): ObjMap<V> {
11+
const result = Object.create(null);
12+
13+
for (const map of maps) {
14+
for (const key of Object.keys(map)) {
15+
result[key] = fn(map[key], key);
16+
}
17+
}
18+
return result;
19+
}

src/language/__tests__/schema-parser-test.ts

+117
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,24 @@ describe('Schema Parser', () => {
230230
});
231231
});
232232

233+
it('Union extension without types', () => {
234+
const doc = parse('extend union HelloOrGoodbye implements Greeting');
235+
expectJSON(doc).toDeepEqual({
236+
kind: 'Document',
237+
definitions: [
238+
{
239+
kind: 'UnionTypeExtension',
240+
name: nameNode('HelloOrGoodbye', { start: 13, end: 27 }),
241+
interfaces: [typeNode('Greeting', { start: 39, end: 47 })],
242+
directives: [],
243+
types: [],
244+
loc: { start: 0, end: 47 },
245+
},
246+
],
247+
loc: { start: 0, end: 47 },
248+
});
249+
});
250+
233251
it('Object extension without fields followed by extension', () => {
234252
const doc = parse(`
235253
extend type Hello implements Greeting
@@ -323,6 +341,36 @@ describe('Schema Parser', () => {
323341
});
324342
});
325343

344+
it('Union extension without types followed by extension', () => {
345+
const doc = parse(`
346+
extend union HelloOrGoodbye implements Greeting
347+
348+
extend union HelloOrGoodbye implements SecondGreeting
349+
`);
350+
expectJSON(doc).toDeepEqual({
351+
kind: 'Document',
352+
definitions: [
353+
{
354+
kind: 'UnionTypeExtension',
355+
name: nameNode('HelloOrGoodbye', { start: 20, end: 34 }),
356+
interfaces: [typeNode('Greeting', { start: 46, end: 54 })],
357+
directives: [],
358+
types: [],
359+
loc: { start: 7, end: 54 },
360+
},
361+
{
362+
kind: 'UnionTypeExtension',
363+
name: nameNode('HelloOrGoodbye', { start: 75, end: 89 }),
364+
interfaces: [typeNode('SecondGreeting', { start: 101, end: 115 })],
365+
directives: [],
366+
types: [],
367+
loc: { start: 62, end: 115 },
368+
},
369+
],
370+
loc: { start: 0, end: 120 },
371+
});
372+
});
373+
326374
it('Object extension do not include descriptions', () => {
327375
expectSyntaxError(`
328376
"Description"
@@ -517,6 +565,26 @@ describe('Schema Parser', () => {
517565
});
518566
});
519567

568+
it('Simple union inheriting interface', () => {
569+
const doc = parse('union Hello implements World = Subtype');
570+
571+
expectJSON(doc).toDeepEqual({
572+
kind: 'Document',
573+
definitions: [
574+
{
575+
kind: 'UnionTypeDefinition',
576+
name: nameNode('Hello', { start: 6, end: 11 }),
577+
description: undefined,
578+
interfaces: [typeNode('World', { start: 23, end: 28 })],
579+
directives: [],
580+
types: [typeNode('Subtype', { start: 31, end: 38 })],
581+
loc: { start: 0, end: 38 },
582+
},
583+
],
584+
loc: { start: 0, end: 38 },
585+
});
586+
});
587+
520588
it('Simple type inheriting multiple interfaces', () => {
521589
const doc = parse('type Hello implements Wo & rld { field: String }');
522590

@@ -574,6 +642,29 @@ describe('Schema Parser', () => {
574642
});
575643
});
576644

645+
it('Simple union inheriting multiple interfaces', () => {
646+
const doc = parse('union Hello implements Wo & rld = Subtype');
647+
648+
expectJSON(doc).toDeepEqual({
649+
kind: 'Document',
650+
definitions: [
651+
{
652+
kind: 'UnionTypeDefinition',
653+
name: nameNode('Hello', { start: 6, end: 11 }),
654+
description: undefined,
655+
interfaces: [
656+
typeNode('Wo', { start: 23, end: 25 }),
657+
typeNode('rld', { start: 28, end: 31 }),
658+
],
659+
directives: [],
660+
types: [typeNode('Subtype', { start: 34, end: 41 })],
661+
loc: { start: 0, end: 41 },
662+
},
663+
],
664+
loc: { start: 0, end: 41 },
665+
});
666+
});
667+
577668
it('Simple type inheriting multiple interfaces with leading ampersand', () => {
578669
const doc = parse('type Hello implements & Wo & rld { field: String }');
579670

@@ -633,6 +724,29 @@ describe('Schema Parser', () => {
633724
});
634725
});
635726

727+
it('Simple union inheriting multiple interfaces with leading ampersand', () => {
728+
const doc = parse('union Hello implements & Wo & rld = Subtype');
729+
730+
expectJSON(doc).toDeepEqual({
731+
kind: 'Document',
732+
definitions: [
733+
{
734+
kind: 'UnionTypeDefinition',
735+
name: nameNode('Hello', { start: 6, end: 11 }),
736+
description: undefined,
737+
interfaces: [
738+
typeNode('Wo', { start: 25, end: 27 }),
739+
typeNode('rld', { start: 30, end: 33 }),
740+
],
741+
directives: [],
742+
types: [typeNode('Subtype', { start: 36, end: 43 })],
743+
loc: { start: 0, end: 43 },
744+
},
745+
],
746+
loc: { start: 0, end: 43 },
747+
});
748+
});
749+
636750
it('Single value enum', () => {
637751
const doc = parse('enum Hello { WORLD }');
638752

@@ -880,6 +994,7 @@ describe('Schema Parser', () => {
880994
kind: 'UnionTypeDefinition',
881995
name: nameNode('Hello', { start: 6, end: 11 }),
882996
description: undefined,
997+
interfaces: [],
883998
directives: [],
884999
types: [typeNode('World', { start: 14, end: 19 })],
8851000
loc: { start: 0, end: 19 },
@@ -899,6 +1014,7 @@ describe('Schema Parser', () => {
8991014
kind: 'UnionTypeDefinition',
9001015
name: nameNode('Hello', { start: 6, end: 11 }),
9011016
description: undefined,
1017+
interfaces: [],
9021018
directives: [],
9031019
types: [
9041020
typeNode('Wo', { start: 14, end: 16 }),
@@ -921,6 +1037,7 @@ describe('Schema Parser', () => {
9211037
kind: 'UnionTypeDefinition',
9221038
name: nameNode('Hello', { start: 6, end: 11 }),
9231039
description: undefined,
1040+
interfaces: [],
9241041
directives: [],
9251042
types: [
9261043
typeNode('Wo', { start: 16, end: 18 }),

src/language/__tests__/schema-printer-test.ts

+22
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ describe('Printer: SDL document', () => {
110110
111111
extend union Feed @onUnion
112112
113+
interface Node {
114+
id: ID
115+
}
116+
117+
interface Resource {
118+
url: String
119+
}
120+
121+
extend type Photo implements Node {
122+
id: ID
123+
url: String
124+
}
125+
126+
extend type Video implements Node {
127+
id: ID
128+
url: String
129+
}
130+
131+
union Media implements Node = Photo | Video
132+
133+
extend union Media implements Resource
134+
113135
scalar CustomScalar
114136
115137
scalar AnnotatedScalar @onScalar

src/language/ast.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,13 @@ export const QueryDocumentKeys: {
262262
'directives',
263263
'fields',
264264
],
265-
UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
265+
UnionTypeDefinition: [
266+
'description',
267+
'name',
268+
'interfaces',
269+
'directives',
270+
'types',
271+
],
266272
EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
267273
EnumValueDefinition: ['description', 'name', 'directives'],
268274
InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
@@ -274,7 +280,7 @@ export const QueryDocumentKeys: {
274280
ScalarTypeExtension: ['name', 'directives'],
275281
ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
276282
InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
277-
UnionTypeExtension: ['name', 'directives', 'types'],
283+
UnionTypeExtension: ['name', 'interfaces', 'directives', 'types'],
278284
EnumTypeExtension: ['name', 'directives', 'values'],
279285
InputObjectTypeExtension: ['name', 'directives', 'fields'],
280286
};
@@ -624,6 +630,7 @@ export interface UnionTypeDefinitionNode {
624630
readonly loc?: Location;
625631
readonly description?: StringValueNode;
626632
readonly name: NameNode;
633+
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
627634
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
628635
readonly types?: ReadonlyArray<NamedTypeNode>;
629636
}
@@ -716,6 +723,7 @@ export interface UnionTypeExtensionNode {
716723
readonly kind: Kind.UNION_TYPE_EXTENSION;
717724
readonly loc?: Location;
718725
readonly name: NameNode;
726+
readonly interfaces?: ReadonlyArray<NamedTypeNode>;
719727
readonly directives?: ReadonlyArray<ConstDirectiveNode>;
720728
readonly types?: ReadonlyArray<NamedTypeNode>;
721729
}

src/language/parser.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -970,12 +970,14 @@ export class Parser {
970970
const description = this.parseDescription();
971971
this.expectKeyword('union');
972972
const name = this.parseName();
973+
const interfaces = this.parseImplementsInterfaces();
973974
const directives = this.parseConstDirectives();
974975
const types = this.parseUnionMemberTypes();
975976
return this.node<UnionTypeDefinitionNode>(start, {
976977
kind: Kind.UNION_TYPE_DEFINITION,
977978
description,
978979
name,
980+
interfaces,
979981
directives,
980982
types,
981983
});
@@ -1249,14 +1251,20 @@ export class Parser {
12491251
this.expectKeyword('extend');
12501252
this.expectKeyword('union');
12511253
const name = this.parseName();
1254+
const interfaces = this.parseImplementsInterfaces();
12521255
const directives = this.parseConstDirectives();
12531256
const types = this.parseUnionMemberTypes();
1254-
if (directives.length === 0 && types.length === 0) {
1257+
if (
1258+
interfaces.length === 0 &&
1259+
directives.length === 0 &&
1260+
types.length === 0
1261+
) {
12551262
throw this.unexpected();
12561263
}
12571264
return this.node<UnionTypeExtensionNode>(start, {
12581265
kind: Kind.UNION_TYPE_EXTENSION,
12591266
name,
1267+
interfaces,
12601268
directives,
12611269
types,
12621270
});

0 commit comments

Comments
 (0)