Skip to content

Commit e47f0ad

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 5f247e0 commit e47f0ad

18 files changed

+428
-16
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ describe('Execute: Union and intersection types', () => {
212212
kind: 'UNION',
213213
name: 'Pet',
214214
fields: null,
215-
interfaces: null,
215+
interfaces: [],
216216
possibleTypes: [{ name: 'Dog' }, { name: 'Cat' }],
217217
enumValues: null,
218218
inputFields: null,

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
});

src/language/printer.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,16 @@ const printDocASTReducer: ASTReducer<string> = {
202202
},
203203

204204
UnionTypeDefinition: {
205-
leave: ({ description, name, directives, types }) =>
205+
leave: ({ description, name, interfaces, directives, types }) =>
206206
wrap('', description, '\n') +
207207
join(
208-
['union', name, join(directives, ' '), wrap('= ', join(types, ' | '))],
208+
[
209+
'union',
210+
name,
211+
wrap('implements ', join(interfaces, ' & ')),
212+
join(directives, ' '),
213+
wrap('= ', join(types, ' | ')),
214+
],
209215
' ',
210216
),
211217
},
@@ -282,11 +288,12 @@ const printDocASTReducer: ASTReducer<string> = {
282288
},
283289

284290
UnionTypeExtension: {
285-
leave: ({ name, directives, types }) =>
291+
leave: ({ name, interfaces, directives, types }) =>
286292
join(
287293
[
288294
'extend union',
289295
name,
296+
wrap('implements ', join(interfaces, ' & ')),
290297
join(directives, ' '),
291298
wrap('= ', join(types, ' | ')),
292299
],

src/type/__tests__/schema-test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
GraphQLList,
1414
GraphQLObjectType,
1515
GraphQLScalarType,
16+
GraphQLUnionType,
1617
} from '../definition';
1718
import { GraphQLDirective } from '../directives';
1819
import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../scalars';
@@ -212,6 +213,30 @@ describe('Type System: Schema', () => {
212213
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
213214
});
214215

216+
it("includes unions's thunk subtypes in the type map", () => {
217+
const SomeUnion = new GraphQLUnionType({
218+
name: 'SomeUnion',
219+
types: () => [SomeSubtype],
220+
interfaces: () => [SomeInterface],
221+
});
222+
223+
const SomeInterface = new GraphQLInterfaceType({
224+
name: 'SomeInterface',
225+
fields: {},
226+
});
227+
228+
const SomeSubtype = new GraphQLObjectType({
229+
name: 'SomeSubtype',
230+
fields: {},
231+
});
232+
233+
const schema = new GraphQLSchema({ types: [SomeUnion] });
234+
235+
expect(schema.getType('SomeUnion')).to.equal(SomeUnion);
236+
expect(schema.getType('SomeInterface')).to.equal(SomeInterface);
237+
expect(schema.getType('SomeSubtype')).to.equal(SomeSubtype);
238+
});
239+
215240
it('includes nested input objects in the map', () => {
216241
const NestedInputObject = new GraphQLInputObjectType({
217242
name: 'NestedInputObject',

0 commit comments

Comments
 (0)